Pascal Newsletter #23 - 23-JUN-2001
INDEX
1. A FEW WORDS FROM THE EDITOR
2. HASH TABLES - By Alirio A. Gavidia B.
- How to take a decision
- The alternatives - order
- Closer to "case..of"
- Pros and cons
- "Hash" tables or lists
- Facing the way to find the index - Multiplication method
- Manipulating strings of characters
- Conclusion
3. KYLIX DESKTOP DEVELOPER EDITION FOR JUST $199!
4. LOADING A JPEG IMAGE FILE INTO A BITMAP
5. PE EXPLORER
6. PESTPATROL
7. HYPHENATION
8. GREATIS TFORMDESIGNER 3.0
9. GETTING THE ICON OF AN APPLICATION OR DOCUMENT
________________________________________________________________________
1. A FEW WORDS FROM THE EDITOR
Before we start, I'd like to apologize for the delay in releasing this
issue of the newsletter.
For those who are interested in a comparison between Delphi and Visual
Basic, please check the last issue of our Developers Newsletter:
http://www.latiumsoftware.com/en/developers/0016.php
At Latium Software we are celebrating our first anniversary! With its
ups and downs, almost without noticing, a full year has elapsed... I
recall that this newsletter started with no more than 100 subscribers,
and after a year we now have more than 2,600! I'd like to thank all of
you, specially those who have accompanied us from the very beginning,
the webmasters who linked to our web site, those who have collaborated
with the newsletter with their articles (particularly to Alirio A.
Gavidia), those who contacted us to congratulate us for the newsletter
and encouraged us to keep it going, those who recommended us to their
colleagues, those who voted for us in the rankings, and finally I don't
want to forget all the companies who trusted us to present their
products. Thank you all very much. In the following year there will be
many changes, hoping to be able to fulfil your expectations much better.
Stay tuned!
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. HASH TABLES - By Alirio A. Gavidia B. <alirio@gavidia.org>
How to take a decision
======================
A common problem in programming is the taking of a course of action
based on the value of a literal sequence of characters. This is simply
to be able to execute an option if for example the variable "Payment" is
'cash', 'check', 'card' or 'transfer'.
We could design a sequence of code like the following:
Const
STransfer = 'transfer';
SCheck = 'check';
SCard = 'card';
SCash = 'cash';
SPayMode = 'Pay mode';
SUnknow = 'unknown';
If pago=SCash then
...
If pago=SCheck then
...
If pago=SCard then
...
If pago=STransfer then
...
This would work, and -to be sincere- I wouldn't worry much about it.
It's practical. Nevertheless, if we speak of more options (like for
example 32 to give a number), it might become a bit uncomfortable and
certainly little elegant.
The option of a "case" is -in this level- unusable, simply because it
requires ordinal types (integers, chars and enumerations). This allows
the "case" statement to be really efficient (unlike the "Case" of xBase
languages).
Improving the previous example we can go to:
If pago=SCash then
...
else If pago= SCheck then
...
else If pago= SCard then
...
else If pago= STransfer then
...
This change prevents that "n" checkings be done for an equal number of
options, but, for the case of the last option -or even worse, the case
in which the option be different from the expected ones-, "n" checkings
are performed anyway.
The alternatives - order
========================
The order seems to be the way. A sorted list is established and then a
binary search is made (divide and conquer). Let us suppose that we have
32 elements: we divide in two sections of 16 elements and select, divide
in two of 8 and select, divide in two of 4 and select, divide in two of
2 and select, and we choose the end. Total: 5 decisions (notice the fact
that 2 to the 5th power is 32).
Until now, the binary search guarantees us that for 32 options a maximum
of 5 (log N) checkings (in fact, 50% of the searches would be solved in
5 checkings, think about it). Nested "if" statements would yield in an
average of 16 checkings (n/2).
Closer to "case..of"
====================
An idea that always appears, and that I have seen used, is to take the
first letter. For the initial example, this would let to us use a "case"
structure, but when arriving to the letter or 'c' of 'cash', 'check' or
'card' in English (or the 't' of 'tarjeta' and 'transferencia' in
Spanish) we would need at least to place an "if" or another "case".
Another approach would be to create a function that gives us a number
for an identifier. For example, I propose the following function:
Function IdentIndex(AIdent: String): integer;
Begin
Result := length(AIdent) + Ord(AIdent[3])
end
sCash 119 // in English
SCheck 106 // in English
SCard 118 // in English
STransfer 105 // in English
sCash 109 // in Spanish
SCheck 107 // in Spanish
SCard 121 // in Spanish
STransfer 110 // in Spanish
Now it's possible to use a "Case":
Var
Ident : string;
begin
Ident := 'cash';
if InputQuery('Hash', SPayMode, Ident) then
case IdentIndex(Ident) of
109: ShowMessage(SCash);
107: ShowMessage(SCheck);
121: ShowMessage(SCard);
110: ShowMessage(STransfer);
else
ShowMessage(SUnknown);
end
end;
Pros and cons
=============
The method is interesting due to the fact that reduces the number of
necessary checkings to one. However now we have the overload of having
to determine the suitable function that returns the indexes. An
incorrect determination of this formula will introduce the following
problems:
* Index repetition
* Scattered indexes
* Slowness in the evaluation of the formula
The last point is very obvious and thus I won't make comments about it.
The repetition of indexes forces us to make additional steps within the
"Case". If we have two identifiers generating the same index, then it's
necessary to make at least one additional analysis to recognize which is
the correct way.
Scattered indexes are a drawback if we try to build a table. In this case
we could design an array like the one I exemplify here:
Begin
:
FillChar(JumpTable,SizeOf(JumpTable), 0);
JumpTable[IdentIndex(SCash)] := PayModeCash;
JumpTable[IdentIndex(SCheck)] := PayModeCheck;
JumpTable[IdentIndex(SCard)] := PayModeCard;
JumpTable[IdentIndex(sTransfer)] := PayModeTransfer;
:
End;
Var
JumpTable : array [100..130] of TRoutine;
Index : Integer;
Ident : string;
Begin
Ident := SCash;
InputQuery('Hash', SPayMode, Ident);
Index := IdentIndex(Ident);
if (Index in [100..130]) and (Assigned(JumpTable[Index])) then
JumpTable[Index]
end;
We have JumpTable as an array of routines that are executed given an
index. But there's a waste since we used only four out of 30 possible
values. The ideal would be that "IdentIndex" gives us four consecutive
values. Sometimes we have to live with that, but sometimes no. In the
case of the analyzer of a compiler, normally it's possible to dedicate
effort to determine the adequate formula without waste. But when there
are many words, or they aren't known beforehand, we must rethink the
whole subject.
"Hash" tables or lists
======================
The formula to determine the index generally must be limited since
usually we can't create arrays with 14-digits indexes. The simplest
solution is dividing and getting the rest. If we want to limit us to
only 8 possibilities, we could divide into 8 and get the rest of the
division.
This way, the new values will be:
sCash 7 // in English
SCheck 2 // in English
SCard 6 // in English
STransfer 1 // in English
sCash 5 // in Spanish
SCheck 3 // in Spanish
SCard 1 // in Spanish
STransfer 6 // in Spanish
Too good, but sometimes it doesn't end up being this way. For example,
limiting it to four instead of eight, index repetition appears:
sCash 3 // in English
SCheck 2 // in English
SCard 2 // in English
STransfer 1 // in English
sCash 1 // in Spanish
SCheck 3 // in Spanish
SCard 1 // in Spanish
STransfer 2 // in Spanish
The suggested limit for a table is usually a prime number that is not
very near to a power of 2.
For tables with repetition of indexes, normally the index is obtained,
the table is checked, and if the cell is free, it is taken, otherwise
the next free cell is searched in a linear way. Each failure in this
search reduces the efficiency of the method.
Hash lists are implemented as lists of lists. Each node of the main list
points to the list of identifiers that match the given index. There are
other solutions to this problem, but I won't analyze them here.
Facing the way to find the index - Multiplication method
========================================================
I like this way of solving the problem when we have a number in a wide
range and we want to reduce it to a smaller range.
For a number of possible values 2^N (2 raised to the Nth power). "Key"
is a value generated directly when evaluating the identifier.
The formula is determined this way:
Hash := ((K*Key) and M) mod S;
Where:
- K is 158 for 8-bits indexes, 40503 for 16-bits indexes and
2654435769 for 16-bits indexes.
- S is 2 raised to the difference between the number of bits and N.
- M is the size of table minus 1. (2^N-1).
For N=10 we have
K=40503 (16 bits)
S=2^(16-10) = 2^6 = 64
M=1023
Hash := ((40503*Key) and 1023) mod 64;
The result would be a value from 0 to 63 (from an original range of 0 to
1023).
Manipulating strings of characters
==================================
Usually we can take each character of the string, add them and take the
module with 256. However this generates problems with anagrams ('abcd'
and 'dcba' for example) and similar words ('cba' and 'cab' for example).
A solution is using xor (exclusive or) instead of the addition and to
add an intermediate table of pseudo-random values that gives us a better
distribution, apart from considering in the algorithm the position of
each letter and not only its intrinsic value.
Conclusion
==========
Using "hash tables" can give terrific results in situations where we
have knowledge of the identifiers to search and/or where we can waste
memory in exchange for speed. In particular, compilers and interpreters
are -in my opinion- the best cases. It's also useful in database systems
where the problem is not space but the need of a minimum amount of
readings. However, a problem to consider is the elimination of records
in a table, something usual in databases.
------------------
The source code that accompanies this article can be downloaded from:
http://www.latiumsoftware.com/en/file.php?id=p23
------------------
To learn more:
* D. E. Knuth, 1973, The Art of Computer Programming, Vol. 3: Sorting
and Searching, Reading, MS: Addison-Wesley.
------------------------------------
Copyright © 2001 by Alirio A. Gavidia B. All rights reserved.
The publication of this material is allowed by any means from anyone
as long as this material is not modified and the original source is
mentioned.
________________________________________________________________________
3. KYLIX DESKTOP DEVELOPER EDITION FOR JUST $199!
Borland is trying to sell as many licenses of Kylix as possible and for
a limited time has significantly lowered the price of Kylix DDE from US
$999 to only just US $199. Yes, a license of Kylix DDE costs only just
US $199! If you are interested in application development under Linux,
this is definitely the right time to buy:
http://shop.borland.com/Product/0,1057,3-15-CQ100479,00.html
The offer expires on August 23, 2001. The order form allows for
international billing addresses, but a United States shipping address
must be provided for shipment. If you don't have an US address -either
yours or provided by a third party- you can check your local Borland
representative to see at how much they are selling it. It's usually more
expensive, but you don't have to worry about transport, customs, etc.
________________________________________________________________________
4. LOADING A JPEG IMAGE FILE INTO A BITMAP
This is a recurrent question in forums and newsgroups. To load a JPEG
image we have to use the "jpeg" unit that comes with Delphi (if you
don't have it installed, I believe you can find it in the "Extras"
directory in the Delphi CD ROM). You can find alternatives in the web,
but I'm going to use this one because it comes with Delphi.
To load a JPEG image in an Image component you can do something like the
following:
uses jpeg;
procedure TForm1.Button1Click(Sender: TObject);
var
jpg: TJpegImage;
begin
jpg := TJpegImage.Create;
try
jpg.LoadFromFile('d:\path\file.jpg');
Image1.Picture.Assign(jpg); // Loads the image as a JPEG
finally
jpg.Free;
end;
end;
The image is loaded as JPEG, which is fine, unless we intend to have
access to the Pixels and ScanLine properties of the Bitmap. For example,
the following won't work:
Image1.Canvas.Pixels[0,0] := clWhite; // Top-leftmost pixel in white
If we intend to perform some image processing we should load the JPEG
image as a bitmap. To do that we have to use the Assing method of a
Bitmap, not a Picture, so we just have to make a little change in the
procedure we presented above:
Image1.Picture.Bitmap.Assign(jpg); // Loads the image as a BMP
________________________________________________________________________
5. PE EXPLORER
PE Explorer is a source code analyzer, resource tool and disassembler.
Allows you to view, edit and repair the internal structure and resources
of PE (portable executable) files (such as EXE, DLL, DRV, BPL, DPL, SYS,
CPL, OCX, SCR and other win32 executables). Visual editing features let
you quickly modify the resources without writing any scripts.
Application : PE Explorer v1.20
Home page : Heaven Tools http://www.heaventools.com
License : Shareware (30-day evaluation)
Download : http://www.heaventools.com/download/pexsetup.zip (~1Mb)
________________________________________________________________________
6. PESTPATROL
PestPatrol detects and removes over 16,000 non-virus pests and is
designed to be used with anti-virus software to provide complete
protection. Runs fast, detecting and removing malicious or unwanted
programs and documents including ANSI Bombs, Anarchy-related,
annoyances, AOL Pests, Back Doors, Carding, Denial of Service, Exploits,
Explosives, Hostile Java and ActiveX, Keyloggers, Mail Bombers, Cracking
Tools, Password Crackers and Capture Tools, Phreaking Tools, Port
Scanners, Remote Control/Admin, Sniffers, Spoofers, Spyware, Trojans and
Trojan Creation Tools, War Dialers, Worms, etc.
Application: PestPatrol
Home page : http://www.pestpatrol.com/
License : Tryware (detects but doesn't remove)
Download : http://consumerdownloads.ca.com/consumer/apps/2008/2/
na_aspy_ca_32_en_ASPYLE_MET_trial.exe (~20.2 MB)
________________________________________________________________________
7. HYPHENATION
Sometimes we need to display or print a text, and we'd like to hyphenate
long words that don't fit at the end of a line, to prevent them from
falling entirely into the next line leaving too much space unused.
The main problem that arises is how to divide a word in syllables. Well,
I really don't know how to syllabicate in English, so I leave that part
to you, but I hope you find the example on Spanish syllabication useful:
procedure Syllabify(Syllables: TStringList; s: string);
const
Consonants = ['b','B','c','C','d','D','f','F','g','G',
'h','H','j','J','k','K','l','L','m','M','n','N',
'ñ','Ñ','p','P','q','Q','r','R','s','S','t','T',
'v','V','w','W','x','X','y','Y','z','Z'];
StrongVowels = ['a','A','á','Á','e','E','é','É',
'í','Í','o','ó','O','Ó','ú','Ú'];
WeakVowels = ['i','I','u','U','ü','Ü'];
Vowels = StrongVowels + WeakVowels;
Letters = Vowels + Consonants;
var
i, j, n, m, hyphen: integer;
begin
j := 2;
s := #0 + s + #0;
n := Length(s) - 1;
i := 2;
Syllables.Clear;
while i <= n do begin
hyphen := 0; // Do not hyphenate
if s[i] in Consonants then begin
if s[i+1] in Vowels then begin
if s[i-1] in Vowels then hyphen := 1;
end else if (s[i] in ['s', 'S']) and (s[i-1] in ['n', 'N'])
and (s[i+1] in Consonants) then begin
hyphen := 2;
end else if (s[i+1] in Consonants) and
(s[i-1] in Vowels) then begin
if s[i+1] in ['r','R'] then begin
if s[i] in ['b','B','c','C','d','D','f','F','g',
'G','k','K','p','P','r','R','t','T','v','V']
then hyphen := 1 else hyphen := 2;
end else if s[i+1] in ['l','L'] then begin
if s[i] in ['b','B','c','C','d','D','f','F','g',
'G','k','K','l','L','p','P','t','T','v','V']
then hyphen := 1 else hyphen := 2;
end else if s[i+1] in ['h', 'H'] then begin
if s[i] in ['c', 'C', 's', 'S', 'p', 'P']
then hyphen := 1 else hyphen := 2;
end else
hyphen := 2;
end;
end else if s[i] in StrongVowels then begin
if (s[i-1] in StrongVowels) then hyphen := 1
end else if s[i] = '-' then begin
Syllables.Add(Copy(s, j, i - j));
Syllables.Add('-');
inc(i);
j := i;
end;
if hyphen = 1 then begin // Hyphenate here
Syllables.Add(Copy(s, j, i - j));
j := i;
end else if hyphen = 2 then begin // Hyphenate after
inc(i);
Syllables.Add(Copy(s, j, i - j));
j := i;
end;
inc(i);
end;
m := Syllables.Count - 1;
if (j = n) and (m >= 0) and (s[n] in Consonants) then
Syllables[m] := Syllables[m] + s[n] // Last letter
else
Syllables.Add(Copy(s, j, n - j + 1)); // Last syllable
end;
To test the procedure yon can drop a Textbox and a Label on a form and
in the Change event of the Textbox write:
procedure TForm1.Edit1Change(Sender: TObject);
var
Syllables: TStringList;
begin
Syllables := TStringList.Create;
try
Syllabify(Syllables, Edit1.Text);
Label1.Caption := StringReplace(Trim(Syllables.Text),
#13#10, '-', [rfReplaceAll]);
finally
Syllables.Free;
end;
end;
Now that we have a syllabication procedure, we have to note that we
can't hyphenate a word in any syllable break. It is usually correct
and/or desirable to join small syllables at the left and/or right sides
of a word to guarantee for example that there are at least two syllables
on either side of the word when it gets hyphenated, or -like in the
following example- to make sure that at least we have four characters in
either side:
procedure ApplyRules(Syllables: TStringList);
// Guarantee there are at least four letters in the left
// and right parts of the word
begin
with Syllables do begin
if Count = 1 then exit;
while Count > 1 do begin
if Length(Strings[0]) >= 4 then break;
Strings[0] := Strings[0] + Strings[1];
Delete(1);
end;
while Syllables.Count > 1 do begin
if Length(Strings[Count-1]) >= 4 then break;
Strings[Count-2] := Strings[Count-2]
+ Strings[Count-1];
Delete(Count-1);
end;
end;
end;
Finally, it comes the time to parse the text separating the lines of
a paragraph determining which words should be hyphenated. The following
example does that with a text to be displayed in a Memo:
procedure Hyphenate(Memo: TMemo; OriginalText: TStrings);
var
paragraph, i, j, k, m, n, MaxLineWidth: integer;
s, line: string;
Bitmap: TBitmap;
Canvas: TCanvas;
Syllables: TStringList;
begin
Syllables := TStringList.Create;
try
// We need a canvas to use its TextWidth method to get the width
// of the text to see if it fits in the client area or not. The
// TMemo class doesn't have a Canvas property, so we have to
// create one of our own.
Bitmap := TBitmap.Create;
Canvas := Bitmap.Canvas;
try
Canvas.Font := Memo.Font;
MaxLineWidth := Memo.ClientWidth - 6; // Maximum width
Memo.Lines.Clear;
for paragraph := 0 to OriginalText.Count - 1 do begin
// For each paragraph
s := OriginalText[paragraph]; // Get the original paragraph
// Get the lines in which we have to break the paragraph
while Canvas.TextWidth(s) > MaxLineWidth do begin
// First we find (in "j") the index of the start of the
// first word that doesn't fit (the one to hyphenate)
j := 1;
n := Length(s);
i := 2;
while i <= n do begin
if (s[i-1] = ' ') and (s[i] <> ' ') then
j := i; // last beginning of a word
if Canvas.TextWidth(Copy(s, 1, i)) > MaxLineWidth then
break; // reached a width that doesn't fit
inc(i);
end;
// Where does the break occurs?
if s[i] = ' ' then begin
// Great! We break on a space
Memo.Lines.Add(Copy(s, 1, i - 1)); // Add the line
s := Copy(s, i + 1, n - i); // Remove the line
end else begin
// We break somewhere in a word. Now, we find (in "k")
// the first space after the word (k)
k := j + 1;
while (k <= n) and (s[k] <> ' ') do inc(k);
// Divide the word in Syllables
Syllabify(Syllables, Copy(s, j, k - j));
ApplyRules(Syllables);
// Check (in "m") how many syllables fit
m := 0;
Line := Copy(s, 1, j-1);
while Canvas.TextWidth(Line + Syllables[m] + '-')
<= MaxLineWidth do begin
Line := Line + Syllables[m];
inc(m);
end;
if (m <> 0) and (Syllables[m-1] <> '-') then begin
// Hyphenate
Line := Line + '-';
j := Length(Line);
if Syllables[m] = '-' then inc(j);
end;
Memo.Lines.Add(Line); // Add the line
s := Copy(s, j, n - j + 1); // Remove the line
end;
end;
Memo.Lines.Add(s); // Add the last line (it fits)
end;
finally
Bitmap.Free;
end;
finally
Syllables.Free;
end;
end;
To test the procedure, drop a Memo component on a form, align it for
example to the top of the form (Align = alTop) and write the following
code in the Resize event of the form:
procedure TForm1.FormResize(Sender: TObject);
var
OriginalText: TStringList;
begin
OriginalText := TStringList.Create;
try
OriginalText.Add('Si se ha preguntado cómo hacen los '
+ 'programas procesamiento de textos para dividir palabras '
+ 'con de guiones al final de una línea, he aquí un '
+ 'ejemplo sencillo (en comparación con los que usan las '
+ 'aplicaciones de procesamiento de textos).');
OriginalText.Add('Este es un segundo párrafo que se provee '
+ 'con fines de ejemplo.');
Hyphenate(Memo1, OriginalText);
finally
OriginalText.Free;
end;
end;
The full source code of this article can be found attached to this
newsletter.
________________________________________________________________________
8. GREATIS TFORMDESIGNER 3.0
What is Greatis TFormDesigner?
------------------------------
TFormDesigner is a component for Delphi 3-5 and C++ Builder 3-5 that
allows you to move and resize any control on your form. Just place a
TFormDesigner component into your form, set Active property to True,
and enjoy - you don't need to prepare your form to use TFormDesigner.
The beauty of TFormDesigner is that it works in RUN-TIME, whereas the
standard form designer works only in design time. To facilitate ease of
use, TFormDesigner replicates the face of the standard Delphi and C++
Builder form designer.
After TFormDesigner has been activated, you can select any control in
your form by Tab key or mouse click and move/resize it.
Features
--------
- Multi-selection
- ActiveX forms compatibility
- MDI forms compatibility
- Locking and protecting any controls
- Align dialog
- Size dialog
- Alignment palette
- Customizable grab handles
- Customizable design grid
- Size/coordinates hints
- Printable User Manual
- Free trial version for Delphi 3-5 and C++ Builder 3-5
- Fully-functional form editor as free demo
Download
--------
Compiled EXE-demo, printable documentation in PDF-format and trial
version for Delphi 3-5 and C++ Builder 3-5 are included in the demo kit,
availabled from: http://www.greatis.com/formdesdemo.zip (~756K)
License
-------
TFormDesigner costs US$ 29.95 for a single-user license.
More information
----------------
You can find more information at TFormDesigner's home page
http://www.greatis.com/formdes.htm
For more information, contact Greatis Software <b-team@greatis.com>
________________________________________________________________________
9. GETTING THE ICON OF AN APPLICATION OR DOCUMENT
Marian Hubinsky sent me this code in response to my article "Getting the
icon of an application or document", telling me that GetAssociatedIcon
(the function I wrote) "doesn't extract correctly some small icons, this
one is smaller and works little bit better":
// get icon from Shell
uses ShellApi;
function SHSmallIcon(xpath : string; getopen : boolean) : hicon;
var
fili : TSHFileInfo;
aka : Integer;
begin
aka := SHGFI_ICON or SHGFI_SMALLICON;
if getopen then aka := aka or SHGFI_OPENICON;
SHGetFileInfo(Pchar(xpath), 0, fili, sizeof(TSHFileInfo), aka);
Result := fili.hIcon;
end;
// example of use:
procedure TForm1.Button3Click(Sender: TObject);
var
smi : hicon;
begin
smi := SHSmallIcon('c:\windows', true);
DrawIconex(Form1.Canvas.Handle, 10, 10, smi, 0, 0, 0, 0, DI_normal);
DestroyIcon(smi);
end;
// Marian Hubinsky <treeumph@tn.sknet.sk> http://wtools32.szm.com
Thanks Marian for your collaboration.
________________________________________________________________________
YOU CAN HELP US!
We need your help to keep growing. The easiest way you can help us is
voting for us in any or some of these rankings:
http://www.programmingpages.com/?r=latiumsoftwarecomenpascal
http://top100borland.com/in.php?who=20
It's just a few seconds for you that really mean much 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=p23
________________________________________________________________________
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.
________________________________________________________________________
|