[go: up one dir, main page]

100% found this document useful (1 vote)
210 views47 pages

June 2001, Volume 7, Number 6: On The Cover

- The API Calls Working with Files and Folders - Bill Todd - On Language Compiler Directives — Cary Jensen, Ph.D. - Sound+Vision Getting Started with OpenGL: Part II — Eli Bar-Yosef - Kylix Quickstart - Kylix and Xlib — Charlie Calvert - On the ’Net - SAX Generation of XML — Keith Wood - Distributed Delphi - IP Helper API: Part I At Your Fingertips Kylix Tips Redux REVIEW DEPARTMENTS

Uploaded by

reader-647470
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
210 views47 pages

June 2001, Volume 7, Number 6: On The Cover

- The API Calls Working with Files and Folders - Bill Todd - On Language Compiler Directives — Cary Jensen, Ph.D. - Sound+Vision Getting Started with OpenGL: Part II — Eli Bar-Yosef - Kylix Quickstart - Kylix and Xlib — Charlie Calvert - On the ’Net - SAX Generation of XML — Keith Wood - Distributed Delphi - IP Helper API: Part I At Your Fingertips Kylix Tips Redux REVIEW DEPARTMENTS

Uploaded by

reader-647470
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 47

June 2001, Volume 7, Number 6

Cover Art By: Arthur Dugoni

ON THE COVER
5 The API Calls 31 Distributed Delphi
Working with Files and Folders — Bill Todd IP Helper API: Part I — John C. Penman
Oddly, Delphi offers insufficient help when it comes to manipulating John Penman explains the basics of the IP Helper API, and then dives
files and folders. But there’s no need to resort to API calls — at least into the specifics of how to retrieve IP configuration details for any
not directly — since Bill Todd has wrapped them up so nicely. adapter on a local machine. It’s the first of a two-article series.
FEATURES
37 At Your Fingertips
13 On Language Kylix Tips Redux — Bruno Sonnino
Compiler Directives — Cary Jensen, Ph.D. Bruno Sonnino revisits some Delphi tips of columns past — rotating
Cary Jensen describes the directives that allow you to control the text, and detecting drives and obtaining information about them —
compiler. From renaming file extensions, to conditionals to handle and gives them a Kylix gloss. One is cross-platform, two are not.
multiple OSes, make sure you’re getting the most from Delphi.

17 Sound+Vision REVIEW
Getting Started with OpenGL: Part II — Eli Bar-Yosef
Eli Bar-Yosef wraps up his series on 3D programming with OpenGL. 41 GExperts
This month he illustrates techniques to create the illusion of lighting, Product Review by Clay Shannon
materials, and texture — and shares a working demo.

21 Kylix Quickstart DEPARTMENTS


Kylix and Xlib — Charlie Calvert 2 Delphi Tools
Charlie Calvert shows how to write variations of the Hello World 44 Best Practices by Clay Shannon
program — from scratch. And we do mean from scratch. There’s nary 46 File | New by Alan C. Moore, Ph.D.
a CLX operation; it’s all about learning the X Window System.

26 On the ’Net
SAX Generation of XML — Keith Wood
Keith Wood introduces the latest Microsoft XML package (MSXML v3)
and its support for SAX generation via its IMXWriter interface, then
demonstrates its use by generating XML from a database.
1 June 2001 Delphi Informant Magazine
Delphi /Xpress Press/ and SecureWave Announce SecureStack 1.0
/Xpress Press/ and SecureWave
T O O L S announced the release of
SecureStack 1.0, a solution to pro-
New Products tect Windows NT/2000 systems
and Solutions from buffer overflow attacks.
Buffer overflow attacks are one
of the biggest security threats
on the Internet. A survey pub-
lished in Information Security
magazine found that 24 percent
of all US companies suffered a
“buffer overflow” attack in the
year 2000. The article is available
at http://www.securewave.com/
html/sec_stack_1.html.
A buffer overflow attack occurs
Book Picks when a hacker overflows an
input buffer on the execution
stack with more data than
the application was designed to
Oracle Net8
Hugo Toledo and Jonathan Gennick accept. Buffer overflow attacks
O’Reilly exploit the lack of bounds check-
ing on the size of input being
stored in a buffer. The attack
data has three elements: An
attacker determines the amount is executed. A buffer overflow dows 2000 with any existing
of data necessary to generate an allows the attacker to take full and future service packs and for
overflow condition; provides an control of your system. hardware platforms with Intel
input string that is executable, SecureStack protects user-mode Pentium, MMX, Pentium Pro,
binary code that will be used applications (including services), Pentium II, Pentium III, or Pen-
to run additional commands but not kernel-mode drivers; is tium IV processors.
ISBN: 1-56592-753-2 on the system; the buffer independent of the server appli-
Cover Price: US$39.95 overflow changes the return cations; supports uni-processor SecureWave
(387 pages)
address to point to the exploit/ machines; and protects from Price: SecureStack Professional, US$249.
malicious code. When the func- remote buffer overflow attacks Contact: sales@securewave.com
tion returns, the exploit code for Windows NT 4.0 and Win- Web Site: http://www.securewave.com
Red Hat Linux 7 Unleashed Extended Systems Releases Advantage Database Server 6.0
Bill Ball, David Pitts, et al.
SAMS Extended Systems is shipping Database Server allows develop- enhanced database security and
Advantage Database Server 6.0, ers the flexibility to combine encryption functionality; and
a high-performance relational SQL statements and relational native SQL in both Advantage
database server (RDBMS). data access methods with the Database Server and Advantage
Enhancements to Advantage performance and control of nav- Local Server.
Database Server 6.0 include igational commands. Advantage Advantage TDataSet
complete referential integrity has native development inter- Descendant replaces the BDE
support, including primary/ faces, optimized data access for easy deployment and fea-
foreign key definition and cas- methodology, and provides secu- tures native components par-
caded updates and deletes. The rity, stability, and data integrity allel to TTable, TQuery, and
ISBN: 0-672-31985-3 Advantage Extended Procedures with no database administrator. TStoredProc. Advantage OLE
Cover Price: US$49.99
(1,112 pages, 2 CD-ROMs)
provide easy-to-develop stored Advantage Database Server 6.0 DB Provider is available for
procedures that are executed at features include free application ADO database access; Advan-
the server. The Advantage Data development tools (TDataSet tage Database Server supports
Architect allows developers to Descendant, OLE DB Provider, Windows 95/98/ME, Windows
import various table types such as ODBC Driver); the capability NT/2000, and Novell Net-
Access, InterBase, Paradox, and of scaling from local to peer- Ware. Thirty-day evaluation
SQL Server to Advantage-com- to-peer to client/server environ- copies are available.
patible tables. ments with no code changes;
Advantage Database Server is royalty-free distribution for local Extended Systems
a high-performance client/server and peer-to-peer environments; Price: Two-user license, US$249;
RDBMS for stand-alone, net- full server-based transaction unlimited-user license, US$7,495.
worked, Internet, and mobile processing; field-level and Contact: (800) 235-7576, ext. 5030
database applications. Advantage record-level constraint support; Web Site: http://www.extendedsystems.com

2 June 2001 Delphi Informant Magazine


Delphi Borland Releases Kylix
Borland Software Corp. Delphi and C++Builder, CLX Hat, SuSE and Mandrake.
T O O L S announced the availability of should also simplify the Kylix is available in three ver-
Kylix, the first native rapid migration of Windows-based sions: Server Developer for pro-
New Products application development applications onto the Linux fessional and corporate Apache
and Solutions (RAD) environment for the platform. Kylix delivers a com- Web developers, Desktop Devel-
Linux operating system. prehensive palette of over 165 oper for professional application
Kylix combines an intuitive reusable, customizable, and developers, and Open Edition
visual design environment, opti- extensible CLX components for open source and free soft-
mizing native code compiler, for out-of-box productivity. ware (GPL) development only.
interactive debugger, and compre- Apache developers can accel- Kylix Open Edition will be
hensive component suite to give erate Web server development available by mid-2001.
developers tools to deliver Web, with NetCLX. NetCLX com-
desktop, and database applica- bines browser, server, and data- Borland Software Corp.
tions on Linux. Corporate and base development technologies Price: Server Developer, US$1,999; Desk-
individual developers can now to deliver scalable Web appli- top Developer, US$999; Open Edition, free
take advantage of the flexibility of cations that support a large download or US$99 with hardcopy documen-
Book Picks cross-platform capabilities and a number of users and large vol- tation and CD.
native RAD environment. umes of data. Kylix supports Contact: (800) 632-2864
With Kylix, developers can major Linux distributions, Red Web Site: http://www.borland.com/kylix
SAMS Teach Yourself Microsoft create sophisticated high-per-
SQL Server 7.0 formance Linux applications
Richard Waymire and Rick Sawtell
Cocolsoft Computer Solutions Announces TColDelphi
with the ease of drag-and-drop
SAMS
development. Kylix includes Cocolsoft Computer Solu- tation for Delphi code, as a
CLX, the component library tions announced TColDelphi, code-formatting tool, or for
for cross-platform develop- a component for Delphi that writing an Interface unit (thus
ment. With CLX, developers parses Delphi Pascal code. ignoring the implementation).
can exploit the power of TColDelphi fires events for TColDelphi has been obfus-
component-based development grammar non-terminals (such as cated with CocolCloak.
to radically speed time- Interface, UnitsName, VarIdent)
to-market for high-perfor- and tokens. The component has Cocolsoft Computer Solutions
mance Web, database, and 97 events, 18 properties, 12 Price: TColDelphi Budget, US$30;
ISBN: 0-672-31290-5 desktop applications. Modeled methods, and a Help file. TColDelphi Standard, US$50.
Cover Price: US$39.99 after the award-winning com- Some uses of this component Contact: info@cocolsoft.com.au
(772 pages)
ponent library of Borland are as ‘JavaDoc’-like documen- Web Site: http://www.cocolsoft.com.au

Red Brook Software Releases SQL Tester 1.2


Microsoft SQL Server 2000 DTS Red Brook Software, Inc.
Timothy Peterson announced the release of SQL
SAMS
Tester 1.2, an upgrade to Red
Brooks’ SQL statement testing
and management utility. SQL
Tester 1.2 includes several new
features, including reading SQL
statements from and writing
SQL statements to query objects
in Delphi form files (DFMs);
testing SQL statements in
ISBN: 0-672-32011-8 batches; exporting query results
Cover Price: US$49.99 to ASCII text and XML files; and
(706 pages) displaying all database properties.
These features allow developers
to read SQL statements from
DFMs, batch test, make cor-
rections, and write corrected
statements back to the DFMs. to assign SQL statements to through ADO data link files
As with manually entered SQL the SQL property at run time. with OLE DB providers or BDE
statements, both standard and SQL statements are automati- aliases with SQL Links.
parameterized queries can be cally numbered and stored with
fully tested. identification information. Red Brook Software, Inc.
For developers using SQL files, SQL Tester can access SQL Price: US$195
SQL Tester makes it easy to use Server, Oracle, InterBase, Par- Phone: (518) 786-3199
Delphi’s LoadFromFile method adox, and Access databases Web Site: http://www.redbrooksoftware.com

3 June 2001 Delphi Informant Magazine


Delphi Core Lab Releases Oracle Data Access Components NET Edition
Core Lab announced the
T O O L S release of Oracle Data Access
Components NET Edition, a
New Products library of native VCL/CLX com-
and Solutions ponents for direct access to
Oracle database server.
ODAC NET works directly
through TCP/IP and therefore
doesn’t require Oracle software
on the client side. This allows
developers to build thin-client
applications. ODAC NET
allows the simplification of
application distribution and
administration and reduces
system requirements. ODAC
Book Picks NET supports access to Oracle
that uses OCI as well. It is easy
to switch to any NET or stan-
Professional SQL Server 2000
dard way of accessing data.
Programming ODAC aids faster development objects, arrays, nested tables, Builder 4, 5; and Kylix. A
Robert Vieira of database applications with BLOB and CLOB data types, free trial version is available for
Wrox Oracle. It supports many Ora- speeding up fetch record from download.
cle-specific features and simplifies database, advanced design-time
the development of client/server editors, embedded SQL Designer Core Lab
applications. ODAC gives all the to build queries, and more. Price: ODAC 3.00 Standard, US$99; NET
advantages of processing data ODAC supports Oracle 7.3, 8, Edition, US$149; Professional, including
such as flexible automatic updat- and 8i including Personal Oracle. source code, US$345.
ing, advanced locking and refresh Core Lab distributes ODAC Contact: odac@crlab.com
rows, advanced support of Oracle packages for Delphi 4, 5; C++ Web Site: http://www.crlab.com

ISBN: 1-861004-48-6
HTML Shrinker Pro 2.0 Reduces Web or WAP Site Size
Cover Price: US$59.99 Harald Heim Software PL, and PHP. It shrinks plain users to select single files or to
(1,391pages) announced the release of HTML HTML, WML and XML code, shrink an entire Web site with one
Shrinker Pro 2.0, a tool for and also Javascript, PHP, Perl, click. Users can replace the orig-
reducing the size of various Web, WMLscript, and VBScript code. inal files with those of reduced
Windows 2000 Quick Fixes WAP, and script files, to speed Comments from different size while making backups, or
Jim Boyce Web or WAP site loading, reduce HTML editors such as Front- can shrink files to a destination
O’Reilly
Web space and bandwidth, and Page, Dreamweaver, PageMill, folder while the original files stay
consequently save Web hosting and SourceSafe are supported untouched. Users can also shrink
fees. and SSI commands are left files or whole folder trees directly
HTML Shrinker Pro 2.0 untouched. HTML Shrinker 2.0 from the Windows Explorer by
can reduce the size of various shrinks files up to 50 percent using the context menu.
file types, e.g. HTML, WML, and allows them to be restored at HTML Shrinker Pro 2.0
WMLS, SHTML, JHTML, any time. also allows users to clean up
XML, CFML, JS, CSS, ASP, HTML Shrinker Pro 2.0 allows HTML documents by removing
all images, scripts, styles, NB
ISBN: 0-596-00017-0 spaces, or font tags to make them
Cover Price: US$29.95 slim for archiving or transfer to
(285 pages) pocket computers.
HTML Shrinker Pro 2.0 is
available for Windows 95, 98,
Me, NT, and 2000. A feature-
limited Light Version of HTML
Shrinker is available as freeware.

Harald Heim Software


Price: Commercial license, US$49.95; per-
sonal license (for use on non-profit Web
sites), US$29.95.
Contact: info@thepluginsite.com
Web Site: http://www.thepluginsite.com

4 June 2001 Delphi Informant Magazine


The API Calls
File and Folder Manipulation / Win API / Wrapper Components / Delphi 4, 5

By Bill Todd

Working with Files and Folders


Better Living through Windows API Wrappers

C reating, moving, copying, renaming, deleting, and searching folders are common
tasks, yet Delphi provides no components to make these jobs easy. Instead, you
have to resort to the Windows API for most of these functions. In this article, you’ll
create two components. The first, TdgFileOperation, will let you copy, move, delete,
rename, or create entire directory trees. The second, TdgFileFinder, will search any
directory tree for the file(s) you specify. It allows you to attach event handlers to
events that fire before and after each directory is scanned, and when a requested
file is found.

Delphi does provide some run-time library rou- The first member of this structure is the window
tines for working with folders; these routines are handle used to display information about the prog-
shown in Figure 1. However, these routines are ress of the operation. The second parameter is the
limited in several ways. First, there’s no function operation to perform, specified by one of these con-
to copy a folder or files within a folder. Second, stants: FO_COPY, FO_MOVE, FO_DELETE, or
there’s no way to move a folder. Third, the routines FO_RENAME. The third member, pFrom, is a
that let you delete a folder require the folder to be pointer to a null-terminated string that identifies
empty. You can overcome all these limitations — the source folder or files on which to operate. The
and more — by using the Windows API. next member, pTo, is a pointer to a null-terminated
string that contains the destination folder or file.
TdgFileOperation The fFlags member is used to set options that con-
The most powerful and flexible tool in the Win- trol the operation, and is discussed in more detail
dows API for manipulating files and folders is the later in this article. The next member of the struc-
ShFileOperation function. ShFileOperation takes a ture, fAnyOperationsAborted, is set to True if the
single parameter of type LPSHFILEOPSTRUCT, user cancels the operation. The hNameMappings
that is, a pointer to SHFILEOPSTRUCT. Figure member is a handle to a file-name mapping object
2 shows the C-language declaration for that lets you specify the old and new names of indi-
SHFILEOPSTRUCT. vidual files on which to operate. The final structure
member, lpszProgressTitle, contains the title string to
Method Description
be displayed in the progress dialog box.
ChDir Changes the current directory
CreateDir Creates a subdirectory ShFileOperation is much more powerful than it
DirectoryExists Returns True if the directory exists
may appear on first examination. If you specify
ForceDirectories Creates a directory tree
GetCurrentDir Returns the current directory
a path to a folder in pFrom and pTo and
GetDir Returns the current directory for a specified choose FO_COPY as the operation, the source
drive folder and all the files and folders it contains
MkDir Creates a subdirectory will be copied to the destination folder. The
RemoveDir Deletes an empty subdirectory same thing happens if you choose FO_MOVE;
RmDir Deletes an empty subdirectory the entire folder tree will be moved. Choosing
SetCurrentDir Changes the current directory FO_DELETE deletes the entire source folder
Figure 1: Delphi’s folder manipulation routines. tree and all the files it contains.

5 June 2001 Delphi Informant Magazine


The API Calls

typedef struct _SHFILEOPSTRUCT { function TdgFileOperation.DoFileOp(FileOp: UINT): Boolean;


HWND hwnd; var
UINT wFunc; SHFileOpStruct: TSHFileOpStruct;
LPCSTR pFrom; Flags: FILEOP_FLAGS;
LPCSTR pTo; SourceDir: PChar;
FILEOP_FLAGS fFlags; TargetDir: PChar;
BOOL fAnyOperationsAborted; ProgressTitle: PChar;
LPVOID hNameMappings; begin
LPCSTR lpszProgressTitle; Result := False;
} SHFILEOPSTRUCT, FAR *LPSHFILEOPSTRUCT; ProgressTitle := AllocMem(Length(FProgressTitle) + 2);
try
SourceDir := AllocMem(Length(FSourceFolder) + 2);
Figure 2: The declaration for SHFILEOPSTRUCT. try
TargetDir := AllocMem(Length(FTargetFolder) + 2);
try
published StrCopy(ProgressTitle, PChar(FProgressTitle));
property FilesOnly: Boolean StrCopy(SourceDir, PChar(FSourceFolder));
read FFilesOnly write FFilesOnly default False; StrCopy(TargetDir, PChar(FTargetFolder));
property ProgressTitle: string Flags := FOF_NOCONFIRMATION;
read FProgressTitle write FProgressTitle; if FRenameOnCollision then
property RenameOnCollision: Boolean Flags := Flags or FOF_RENAMEONCOLLISION;
read FRenameOnCollision write FRenameOnCollision if FFilesOnly then
default True; Flags := Flags or FOF_FILESONLY;
property ShowProgress: Boolean read FShowProgress if not FShowProgress then
write FShowProgress default True; Flags := Flags or FOF_SILENT
property SourceFolder: string else
read FSourceFolder write FSourceFolder; Flags := FOF_SIMPLEPROGRESS;
property TargetFolder: string with SHFileOpStruct do begin
read FTargetFolder write FTargetFolder; Wnd := Application.Handle;
end; wFunc := FileOp;
pFrom := SourceDir;
pTo := TargetDir;
Figure 3: Additional published properties. fFlags := Flags;
fAnyOperationsAborted := False;
hNameMappings := nil;
FO_RENAME renames the from folder to the name in pTo. You can lpszProgressTitle := ProgressTitle;
also perform these operations on individual files by providing the file if SHFileOperation(SHFileOpStruct) <> 0 then
RaiseLastWin32Error;
names in the pFrom and pTo members of the SHFILEOPSTRUCT. If Result := fAnyOperationsAborted;
you want to operate on groups of files, you can use wildcards. If you end;
cannot specify the files on which you want to operate using wildcards, finally
pFrom can contain a null-separated list of individual file names, and FreeMem(TargetDir, Length(FTargetFolder) + 2);
end;
pTo can contain a matching null-separated list of file names.
finally
FreeMem(SourceDir, Length(FSourceFolder) + 2);
To write a component wrapper around ShFileOperation, start by end;
choosing the File | New | New Component wizard. Use TComponent finally
for the ancestor, and TFileOperation for the class name. Next, add the FreeMem(ProgressTitle, Length(FProgressTitle) + 2);
end;
properties shown in Figure 3. end;

The FilesOnly property lets you specify that operations on files using
wildcards will affect only files and not folders. ProgressTitle lets you Figure 4: The DoFileOp method.
specify a title for the progress dialog box. If RenameOnCollision is
True, any file whose name conflicts with the name of an existing file and TargetFolder contain null-separated lists of file names, the list
in the target folder will be renamed. SourceFolder and TargetFolder must end with two nulls. AllocMem not only allocates the required
contain the names of the folders or files on which to operate. memory, but also fills it with nulls so the nulls at the end
will be there. If you need to use the ability to have multiple
Since all the operations supported by ShFileOperation use the same file names separated by nulls in the pFrom and pTo members
parameters (with the exception of the function to be performed), the of SHFILEOPSTRUCT, you’ll need to change the SourceFolder
TFileOperation component needs just one method, DoFileOp, to make and TargetFolder properties from the string to TStrings, create a
the call to ShFileOperation. Figure 4 shows the code for DoFileOp. StringList to hold the value of each property, and write a method
to convert the contents of the StringList to a null-delimited, double-
DoFileOp takes a single parameter which can have a value null-terminated PChar. Since I’ve never needed to use lists of explicit
of FO_COPY, FO_MOVE, FO_DELETE, or FO_RENAME. file names, I didn’t implement the capability in this component.
DoFileOp is a function that returns False if the user cancels
the operation and True in all other cases. The code begins by The next step is to set the Flags variable in which each bit corresponds
converting the string properties ProgressTitle, SourceFolder, and to one of the flag options. The FOF_NOCONFIRMATION flag
TargetFolder to null-terminated strings. AllocMem is called to allo- is always included to suppress any dialog boxes asking the user to
cate memory for the three PChar variables. In each case, the amount confirm the file operation. The following if statements set the flags
of memory allocated is equal to the length of the string plus two that correspond to the component’s Boolean properties. Once the
bytes. The additional two bytes are required because if SourceFolder Flags variable is set, all members of the SHFILEOPSTRUCT record

6 June 2001 Delphi Informant Magazine


The API Calls

property CancelScan: Boolean


read FCancelScan write FCancelScan;
published
property IncludeSystemFiles: Boolean
read FIncludeSystemFiles write FIncludeSystemFiles
default False;
property AfterDirectoryScan: TAfterDirectoryScan
read FAfterDirectoryScan write FAfterDirectoryScan;
property BeforeDirectoryScan: TBeforeDirectoryScan
read FBeforeDirectoryScan write FBeforeDirectoryScan;
property OnFileFound: TOnFileFound
read FOnFileFound write FOnFileFound;
end;

Figure 5: The FileOp demonstration program’s main form. Figure 7: TdgFileFinder properties.

type procedure TdgFileFinder.FindFiles(const Path: string;


EFileFinderError = class(Exception); const Mask: string; const ScanSubdirectories: Boolean);
{ Searches the specified directory tree for files that
TOnFileFound = procedure(const Path: string;
match the specified path. Parameters:
const FileName, ShortName, AttributeString: string;
Path: The path to the directory in which to start
const Created, Accessed, Updated: TDateTime;
the search. If the path is a null string,
const FileSize: Int64; var ContinueSearch: Boolean)
the current directory is used.
of object;
Mask: The mask that describes the files for which to
TBeforeDirectoryScan = procedure(const Path: string; search. DOS wildcards (* and ?) may be used.
const FileName, ShortName: string; If the mask is a null string, *.* is used. }
var ContinueSearch: Boolean) of object; var
TAfterDirectoryScan = procedure(const Path: string; SearchPath: string;
const FileName, ShortName: string; begin
const LastFileCreated, LastFileAccessed, FCancelScan := False;
LastFileUpdated: TDateTime; const DirSize: Int64; { If no path is supplied, use the current directory. }
var ContinueSearch: Boolean) of object; if Path = '' then
GetDir(0, SearchPath)
else
Figure 6: Exception and event type declarations. begin
if not DirectoryExists(Path) then begin
raise EFileFinderError.Create(
are assigned and ShFileOperation is called. ShFileOperation returns 'The folder ' + Path + ' does not exist.');
zero if the operation is completed successfully, and an error code if Exit;
end; // if
it isn’t. Calling RaiseLastWin32Error converts the Windows error to
SearchPath := Path;
an exception. end; // if
{ Append a backslash to the path. }
The last step in creating this component is to add the public if SearchPath[Length(SearchPath)] <> '\' then
SearchPath := SearchPath + '\';
methods DoCopy, DoMove, DoDelete, DoRename, and DoCreate. The
{ If no mask is supplied, use *.*. }
first four methods call the private method DoFileOp, and pass the if Mask = '' then
constant that corresponds to the operation. DoCreate calls the Delphi ScanDirectory(SearchPath, '*.*', ScanSubdirectories)
run-time library function ForceDirectories to create the path contained else
in the SourceFolder property. The complete source code for the ScanDirectory(SearchPath, Mask, ScanSubdirectories);
end;
TFileOperation component is shown in Listing One (on page10).
Figure 5 shows the main form of the FileOp demonstration pro-
gram that uses this component. You can either type a path in Figure 8: The FindFiles method.
the Source and Destination edit boxes, or choose a folder using the
directory list and click the Set Source or Set Dest. button to have
the path assigned to the corresponding edit box. Enter a title for As with TdgFileOperation, the first step is to use the component
the progress dialog box if you wish, set the checkboxes, and click wizard to create a new component that descends from TComponent.
the button for the operation you want to perform. The next step is to add the type declarations for a custom exception
class and three events, as shown in Figure 6. To make it easy to trap
TdgFileFinder for any exceptions raised by the TdgFileFinder, all exceptions will be
Since Delphi provides the FindFirst and FindNext functions raised as EFileFinderError exceptions. This makes trapping exceptions
(which are wrappers around the Windows API functions raised by this component in try..except and try..finally blocks easy
FindFirstFile and FindNextFile), you don’t need to resort to the and unambiguous.
Windows API to search for files. However, these are relatively low-
level functions and require a significant amount of code if you want This component will fire three different events, all of which
to do anything other than find a specific file by name when you require a set of parameters different from any of the event handler
know the folder that contains the file. The ideal solution is to create types defined in Delphi. The OnFileFound event will be fired each
a custom component that will search an entire directory tree for time a file that matches the search criteria is found. The path,
files, and generate events that let you attach code that will execute file name, short name, attributes as a string, date created, date
before and after a directory is searched, or when a file is found. last accessed, date last updated, and the file’s size are passed as

7 June 2001 Delphi Informant Magazine


The API Calls

TSearchRec = record function TdgFileFinder.Win32FileTimeToDateTime(


Time: Integer; FTime: _FILETIME): TDateTime;
Size: Integer; var
Attr: Integer; LocalTime: _FILETIME;
Name: TFileName; SysTime: _SYSTEMTIME;
ExcludeAttr: Integer; begin
FindHandle: THandle; FileTimeToLocalFileTime(_FILETIME(FTime), LocalTime);
FindData: TWin32FindData; FileTimeToSystemTime(LocalTime, SysTime);
end; Result := SystemTimeToDateTime(SysTime);
end;
TWin32FindData = packed record
dwFileAttributes: Integer;
ftCreationTime: Int64; Figure 10: The Win32FileTimeToDateTime method.
ftLastAccessTime: Int64;
ftLastWriteTime: Int64;
function TdgFileFinder.GetWindowsErrorMsg(
nFileSizeHigh: Integer;
ErrorCode: DWORD): string;
nFileSizeLow: Integer;
const
dwReserved0: Integer;
BuffSize = 1024;
dwReserved1: Integer;
var
cFileName: array[0..259] of Char;
MsgBuff: PChar;
cAlternateFileName: array[0..13] of Char;
begin
end;
MsgBuff := AllocMem(BuffSize);
try
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nil,
Figure 9: TSearchRec and TWin32FindData.
ErrorCode, 0, MsgBuff, BuffSize, nil);
Result := StrPas(MsgBuff);
parameters to the event handler. In addition, a var parameter, finally
ContinueSearch, is passed. By setting this parameter to False, the FreeMem(MsgBuff);
end; // try
event handler can stop the search.
end;

The BeforeDirectoryScan event is fired before scanning the files in procedure TdgFileFinder.RaiseWindowsError(
a directory. The path, file name, and short name of the directory ErrorCode: DWORD);
are passed as parameters with the var parameter ContinueSearch. begin
if ErrorCode <> ERROR_NO_MORE_FILES then
AfterDirectoryScan is triggered after all the files in a directory have raise EFileFinderError.Create(GetWindowsErrorMsg(
been scanned. The parameters are the path, file name of the direc- ErrorCode) + ' (' + IntToStr(ErrorCode) + ')');
tory, short name, creation date of the most recently created file, end;
access date of the most recently accessed file, update date of the most
recently updated file, total size of all the files that met the search
Figure 11: RaiseWindowsError and GetWindowsErrorMsg.
criteria, and ContinueSearch.

Figure 7 shows the component’s properties. The first property, calling Application.ProcessMessages, and then checks to see if the
CancelScan, is a run-time property that’s set to False by default. If CancelScan property has been set to True. This allows you to add
it’s set to True while a file search is in progress, the search will termi- a Cancel button to your application so users can stop the directory
nate at the beginning of the next directory. IncludeSystemFiles is a scan. The next block of code checks the path to see if it’s the root
published property that’s False by default. Set this property to True directory. If so, the long and short names are set to backslash. If not,
to include files that have hidden and system attributes. The last FindFirst is called with its attribute parameter set to faDirectory to
three properties are the event properties for the AfterDirectoryScan, find the record for this directory.
BeforeDirectoryScan, and OnFileFound events.
When you call FindFirst or FindNext, the information about the
Figure 8 shows TdgFileFinder’s only public method, FindFiles. file or directory is returned in the TSearchRec variable passed as
FindFiles sets the CancelScan property to False, and then checks the the third parameter. Figure 9 shows the declaration of TSearchRec.
Path parameter to see if it’s an empty string. If Path is empty, the After the call to FindFirst, the DirLongName and DirShortName
call to GetDir sets the variable SearchPath to the current directory, variables are set from the cFileName and cAlternateFileName mem-
otherwise a call to DirectoryExists verifies that the directory in Path bers of TSearchRec.
exists. If the directory exists, it’s assigned to SearchPath, otherwise
an exception is raised. If SearchPath doesn’t end with a backslash, Next, the BeforeDirectoryScan event is fired, and the value of
one is added to the end of the string. Finally, if the Mask parameter ContinueSearch is checked. If the event handler has set
is a null string, ScanDirectory is called with a mask of *.*, otherwise ContinueSearch to False, Result is set to False and the method
ScanDirectory is called with its second parameter set to Mask. The exits. Next, FindFirst is called with the Path and Mask to find the
third parameter of FindFiles is passed as the third parameter to first file that matches the Mask. Then, a while loop begins that
ScanDirectory. If this parameter, ScanSubdirectories, is True, the continues as long as the variable SearchResult is zero; SearchResult
entire directory tree below the directory in the Path parameter will is the value returned by FindFirst and FindNext which indicates
be scanned. If ScanSubdirectories is False, only the directory passed that a file was found. If the IncludeSystemFiles property is False,
in Path will be searched. and the file has the hidden and system attributes, FindNext is
called followed by Continue to jump back to the beginning of
Listing Two (beginning on page 10) contains the ScanDirectory the while loop. Now that a file that matches the search criteria
method that’s the heart of the component. ScanDirectory starts by has been found, the name and date values are extracted from the

8 June 2001 Delphi Informant Magazine


The API Calls
After the while loop, the call to FindClose releases the memory
allocated by FindFirst. If Result is False, the search has been
cancelled and the method exits; otherwise, the AfterDirectoryScan
event is fired. If the ScanSubdirectories property is True, FindFirst
is called again with a mask of *.* and an attribute of faDirectory to
find the first subdirectory in the directory being searched. Then,
a while loop begins that will search all the subdirectories in the
current directory by calling ScanDirectory recursively followed by
FindNext, until all the subdirectories have been processed. Note
that the while loop begins by checking the directory name and
skipping the directory if its name is “.” (the current directory) or
“..” (the parent directory).

Figure 12 shows the main form of a simple test program for this
component. Type in a starting path, click Scan, and information
about all the files and folders in the directory tree starting at the path
Figure 12: TdgFileFinder demonstration application. you entered will be loaded into two ClientDataSets by code in the
OnFileFound and AfterDirectoryScan event handlers. The complete
TSearchRec, and the attribute string is constructed from the bit listing of the TdgFileFinder component is not included in this article
flags in the dwFileAttributes member of TSearchRec. Now, the total due to its length. To see it, just download the files that accompany
variables for the directory are updated and the OnFileFound event this article (see end of article for details).
is fired. Finally, FindNext is called to locate the next file.
Conclusion
ScanDirectory uses two custom methods. The first is Searching, copying, moving, deleting, renaming, and creating
Win32FileTimeToDateTime, which is shown in Figure 10. This folders can all be done in Delphi with enough code and some
function calls the Windows API functions FileTimeToLocal- Windows API calls. However, the beauty of object-oriented pro-
FileTime to convert the file time from UTC time to local gramming is that it allows you to write code once, and create a
time, and FileTimeToSystemTime to convert the time from the couple of easy-to-use components that should meet your file and
file time format to the system time format. Finally, the Delphi folder manipulation needs for all of your applications. ∆
SystemTimeToDateTime function converts the value to
TDateTime format. The two sample projects referenced in this article are available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
The second custom method is RaiseWindowsError, shown in 2001\JUN\DI200106BT.
Figure 11. RaiseWindowsError checks the error code to see if
it’s ERROR_NO_MORE_FILES; if it is, no exception is raised.
Otherwise, an EFileFinderException is raised that displays the
Windows error message and error code. The Windows error mes-
sage is retrieved by a call to GetWindowsErrorMsg, also shown
in Figure 11. This function calls the Windows API function Bill Todd is president of The Database Group, Inc., a database consulting and
FormatMessage to get the error message that corresponds to the development firm based near Phoenix. He is co-author of four database program-
error code. While you can get along without these functions by ming books and over 80 articles, and is a member of Team Borland, providing
calling RaiseLastWin32Error when FindFirst or FindNext returns technical support on the Borland Internet newsgroups. He is a frequent speaker
a value other than zero or ERROR_NO_MORE_FILES, using at Borland Developer Conferences in the US and Europe. Bill is also a nationally
these function makes life easier when you write your application known trainer and has taught Delphi programming classes across the country and
because you know that any exception raised by this component overseas. Bill can be reached at bill@dbginc.com.
will be an EFileFinderError exception.

9 June 2001 Delphi Informant Magazine


The API Calls

begin
Begin Listing One — TFileOperation component Result := DoFileOp(FO_DELETE);
end;
unit FileOperation;
function TdgFileOperation.DoFileOp(FileOp: UINT): Boolean;
interface
var
SHFileOpStruct: TSHFileOpStruct;
uses
Flags: FILEOP_FLAGS;
Windows, Messages, SysUtils, Classes, Graphics, Controls,
SourceDir: PChar;
Forms, Dialogs;
TargetDir: PChar;
ProgressTitle: PChar;
type
begin
TdgFileOperation = class(TComponent)
Result := False;
private
ProgressTitle := AllocMem(Length(FProgressTitle) + 2);
FSourceFolder: string;
try
FTargetFolder: string;
SourceDir := AllocMem(Length(FSourceFolder) + 2);
FShowProgress: Boolean;
try
FProgressTitle: string;
TargetDir := AllocMem(Length(FTargetFolder) + 2);
FFilesOnly: Boolean;
try
FRenameOnCollision: Boolean;
StrCopy(ProgressTitle, PChar(FProgressTitle));
{ Private declarations. }
StrCopy(SourceDir, PChar(FSourceFolder));
function DoFileOp(FileOp: UINT): Boolean;
StrCopy(TargetDir, PChar(FTargetFolder));
protected
Flags := FOF_NOCONFIRMATION;
{ Protected declarations. }
if FRenameOnCollision then
public
Flags := Flags or FOF_RENAMEONCOLLISION;
{ Public declarations. }
if FFilesOnly then
constructor Create(AOwner: TComponent); override;
Flags := Flags or FOF_FILESONLY;
function DoCopy: Boolean;
if not FShowProgress then
function DoCreateFolder: Boolean;
Flags := Flags or FOF_SILENT
function DoDelete: Boolean;
else
function DoMove: Boolean;
Flags := FOF_SIMPLEPROGRESS;
function DoRename: Boolean;
with SHFileOpStruct do begin
published
Wnd := Application.Handle;
{ Published declarations. }
wFunc := FileOp;
property FilesOnly: Boolean read FFilesOnly
pFrom := SourceDir;
write FFilesOnly default False;
pTo := TargetDir;
property ProgressTitle: string
fFlags := Flags;
read FProgressTitle write FProgressTitle;
fAnyOperationsAborted := False;
property RenameOnCollision: Boolean
hNameMappings := nil;
read FRenameOnCollision write FRenameOnCollision
lpszProgressTitle := ProgressTitle;
default True;
if SHFileOperation(SHFileOpStruct) <> 0 then
property ShowProgress: Boolean
RaiseLastWin32Error;
read FShowProgress write FShowProgress
Result := fAnyOperationsAborted;
default True;
end;
property SourceFolder: string
finally
read FSourceFolder write FSourceFolder;
FreeMem(TargetDir, Length(FTargetFolder) + 2);
property TargetFolder: string
end;
read FTargetFolder write FTargetFolder;
finally
end;
FreeMem(SourceDir, Length(FSourceFolder) + 2);
end;
procedure Register;
finally
FreeMem(ProgressTitle, Length(FProgressTitle) + 2);
implementation
end;
end;
uses ShellApi, FileCtrl;
function TdgFileOperation.DoMove: Boolean;
procedure Register;
begin
begin
Result := DoFileOp(FO_MOVE);
RegisterComponents('DI', [TdgFileOperation]);
end;
end;
function TdgFileOperation.DoRename: Boolean;
{ TFileOperation. }
begin
constructor TdgFileOperation.Create(AOwner: TComponent);
Result := DoFileOp(FO_RENAME);
begin
end;
inherited;
FShowProgress := True;
end.
FFilesOnly := False;
FRenameOnCollision := True;
end;
End Listing One
function TdgFileOperation.DoCopy: Boolean;
begin
Result := DoFileOp(FO_COPY);
Begin Listing Two — ScanDirectory method
end; function TdgFileFinder.ScanDirectory(var Path: string;
const Mask: string; const ScanSubdirectories: Boolean):
function TdgFileOperation.DoCreateFolder: Boolean; Boolean;
begin { Called by FindFiles to scan the directory tree. This
Result := ForceDirectories(FSourceFolder); method calls itself recursively. Parameters:
end; Path: The path to the directory to scan.
Mask: The mask for the files to be found.
function TdgFileOperation.DoDelete: Boolean; ScanSubdirectories: True to scan entire directory tree.

10 June 2001 Delphi Informant Magazine


The API Calls

{ end; // with
TSearchRec = record { Fire the BeforeDirectoryScan event. }
Time: Integer; ContinueSearch := True;
Size: Integer; if Assigned(FBeforeDirectoryScan) then begin
Attr: Integer; FBeforeDirectoryScan(Path, DirLongName,
Name: TFileName; DirShortName, ContinueSearch);
ExcludeAttr: Integer; end; // if
FindHandle: THandle; if not ContinueSearch then begin
FindData: TWin32FindData; Result := False;
end; FindClose(SRec);
Exit;
TWin32FindData = packed record end; // if
dwFileAttributes: Integer; { Save the Path length }
ftCreationTime: Int64; PathLength := Length(Path);
ftLastAccessTime: Int64; { Search for all files in the current directory. }
ftLastWriteTime: Int64; SearchResult := FindFirst(Path + Mask, faAnyFile, SRec);
nFileSizeHigh: Integer; if SearchResult <> 0 then
nFileSizeLow: Integer; RaiseWindowsError(SearchResult);
dwReserved0: Integer; try
dwReserved1: Integer; while SearchResult = 0 do begin
cFileName: array[0..259] of Char; { If the found file is not a directory or volume
cAlternateFileName: array[0..13] of Char; label, process it. }
end; if (SRec.Attr and
} (faDirectory or faVolumeID)) = 0 then
var begin
SRec: TSearchRec; with SRec.FindData do begin
SearchResult: Integer; { If IncludeSystemFiles is False and this is a
PathLength: Integer; system file, skip it. }
ContinueSearch: Boolean; if (not FIncludeSystemFiles) and
{ File information. } (dwFileAttributes and
LongName, FILE_ATTRIBUTE_HIDDEN <> 0) and
ShortName: string; (dwFileAttributes and
AttributeStr: string; FILE_ATTRIBUTE_SYSTEM <> 0) then
Created, begin
Accessed, SearchResult := FindNext(SRec);
Updated: TDateTime; if SearchResult <> 0 then
FileSize: Int64; RaiseWindowsError(SearchResult);
{ Directory information. } Continue;
DirLongName, end;
DirShortName: string; { Process this file. }
DirLastCreated, LongName := StrPas(cFileName);
DirLastAccessed, ShortName := StrPas(cAlternateFileName);
DirLastUpdated: TDateTime; if ShortName = '' then
DirSize: Int64; ShortName := LongName;
begin Created :=
Result := True; Win32FileTimeToDateTime(ftCreationTime);
DirSize := 0; Accessed :=
DirLastCreated := 0; Win32FileTimeToDateTime(ftLastAccessTime);
DirLastAccessed := 0; Updated :=
DirLastUpdated := 0; Win32FileTimeToDateTime(ftLastWriteTime);
{ See if the user wants to cancel the scan. } FileSize :=
Application.ProcessMessages; (nFileSizeHigh * MAXINT) + nFileSizeLow;
if FCancelScan then Exit; { Build the string of attibutes. }
{ Get the record for this directory. } AttributeStr := '';
with SRec.FindData do begin if dwFileAttributes and
if ((ExtractFileDrive(Path) <> '') and FILE_ATTRIBUTE_NORMAL = 0 then begin
(Length(Path) = 3)) or (Path = '\') then if dwFileAttributes and
begin FILE_ATTRIBUTE_ARCHIVE <> 0 then
DirLongName := '\'; AttributeStr := AttributeStr + 'A';
DirShortName := '\'; if dwFileAttributes and
end FILE_ATTRIBUTE_COMPRESSED <> 0 then
else AttributeStr := AttributeStr + 'C';
begin if dwFileAttributes and
SearchResult := FindFirst(Copy(Path, 1, FILE_ATTRIBUTE_HIDDEN <> 0 then
Length(Path) - 1), faDirectory, SRec); AttributeStr := AttributeStr + 'H';
try if dwFileAttributes and
if SearchResult <> 0 then FILE_ATTRIBUTE_SYSTEM <> 0 then
RaiseWindowsError(SearchResult); AttributeStr := AttributeStr + 'S';
DirLongName := StrPas(cFileName); if dwFileAttributes and
DirShortName := StrPas(cAlternateFileName); FILE_ATTRIBUTE_OFFLINE <> 0 then
finally AttributeStr := AttributeStr + 'O';
FindClose(SRec); if dwFileAttributes and
end; // try FILE_ATTRIBUTE_READONLY <> 0 then
end; // if AttributeStr := AttributeStr + 'R';
if dwFileAttributes and
FILE_ATTRIBUTE_TEMPORARY <> 0 then
AttributeStr := AttributeStr + 'T';
end; // if

11 June 2001 Delphi Informant Magazine


The API Calls

end; // with end; // with


{ Fire the BeforeDirectoryScan event. } { Update the totals for the current directory. }
ContinueSearch := True; DirSize := DirSize + FileSize;
if Assigned(FBeforeDirectoryScan) then begin if DirLastCreated < Created then
FBeforeDirectoryScan(Path, DirLongName, DirLastCreated := Created;
DirShortName, ContinueSearch); if DirLastAccessed < Accessed then
end; // if DirLastAccessed := Accessed;
if not ContinueSearch then begin if DirLastUpdated < Updated then
Result := False; DirLastUpdated := Updated;
FindClose(SRec); { Fire the OnFileFound event. }
Exit; ContinueSearch := True;
end; // if if Assigned(FOnFileFound) then begin
{ Save the Path length } FOnFileFound(Path, LongName, ShortName,
PathLength := Length(Path); AttributeStr, Created, Accessed, Updated,
{ Search for all files in the current directory. } FileSize, ContinueSearch);
SearchResult := FindFirst(Path + Mask, faAnyFile, SRec); end; // if
if SearchResult <> 0 then if not ContinueSearch then begin
RaiseWindowsError(SearchResult); Result := False;
try Break;
while SearchResult = 0 do begin end; // if
{ If the found file is not a directory or volume end; // if
label, process it. } { Find the next file. }
if (SRec.Attr and SearchResult := FindNext(SRec);
(faDirectory or faVolumeID)) = 0 then if SearchResult <> 0 then
begin RaiseWindowsError(SearchResult);
with SRec.FindData do begin end; // while
{ If IncludeSystemFiles is False and this is a finally
system file, skip it. } FindClose(SRec);
if (not FIncludeSystemFiles) and end; // try
(dwFileAttributes and if not Result then
FILE_ATTRIBUTE_HIDDEN <> 0) and Exit;
(dwFileAttributes and { Fire the AfterDirectoryScan event. }
FILE_ATTRIBUTE_SYSTEM <> 0) then ContinueSearch := True;
begin if Assigned(FAfterDirectoryScan) then
SearchResult := FindNext(SRec); FAfterDirectoryScan(Path, DirLongName, DirShortName,
if SearchResult <> 0 then DirLastCreated, DirLastAccessed, DirLastUpdated,
RaiseWindowsError(SearchResult); DirSize, ContinueSearch);
Continue; if not ContinueSearch then begin
end; Result := False;
{ Process this file. } Exit;
LongName := StrPas(cFileName); end;
ShortName := StrPas(cAlternateFileName); { Find subdirectories in the current directory. }
if ShortName = '' then if ScanSubdirectories then begin
ShortName := LongName; SearchResult :=
Created := FindFirst(Path + '*.*', faDirectory, SRec);
Win32FileTimeToDateTime(ftCreationTime); if SearchResult <> 0 then
Accessed := RaiseWindowsError(SearchResult);
Win32FileTimeToDateTime(ftLastAccessTime); try
Updated := while SearchResult = 0 do begin
Win32FileTimeToDateTime(ftLastWriteTime); if (SRec.Attr and faDirectory) <> 0 then
FileSize := { If this is the current directory or parent
(nFileSizeHigh * MAXINT) + nFileSizeLow; directory symbol skip it. }
{ Build the string of attibutes. } if (SRec.Name <> '.') and
AttributeStr := ''; (SRec.Name <> '..') then begin
if dwFileAttributes and { Add the directory name to the path. }
FILE_ATTRIBUTE_NORMAL = 0 then begin Path := Path + SRec.Name + '\';
if dwFileAttributes and { Scan the directory. }
FILE_ATTRIBUTE_ARCHIVE <> 0 then if not ScanDirectory(Path, Mask,
AttributeStr := AttributeStr + 'A'; ScanSubdirectories) then begin
if dwFileAttributes and Result := False;
FILE_ATTRIBUTE_COMPRESSED <> 0 then Break;
AttributeStr := AttributeStr + 'C'; end;
if dwFileAttributes and { Delete the directory name from the path. }
FILE_ATTRIBUTE_HIDDEN <> 0 then Delete(Path, PathLength + 1, 255);
AttributeStr := AttributeStr + 'H'; end;
if dwFileAttributes and SearchResult := FindNext(SRec);
FILE_ATTRIBUTE_SYSTEM <> 0 then if SearchResult <> 0 then
AttributeStr := AttributeStr + 'S'; RaiseWindowsError(SearchResult);
if dwFileAttributes and end; // while
FILE_ATTRIBUTE_OFFLINE <> 0 then finally
AttributeStr := AttributeStr + 'O'; FindClose(SRec);
if dwFileAttributes and end; // try
FILE_ATTRIBUTE_READONLY <> 0 then end; // if
AttributeStr := AttributeStr + 'R'; end;
if dwFileAttributes and
FILE_ATTRIBUTE_TEMPORARY <> 0 then End Listing Two
AttributeStr := AttributeStr + 'T';
end; // if

12 June 2001 Delphi Informant Magazine


On Language
Compiler Directives / Delphi 1-5

By Cary Jensen, Ph.D.

Compiler Directives
Controlling the Delphi Compiler

T hey look similar to comments, but they aren’t. And nearly every project has at
least one, even the default project Delphi creates when you first load it. They are
compiler directives, and they are the focus of this month’s article.

You use compiler directives to control how Delphi’s turned on when the directive is followed by a
compiler performs its task. For example, the {$R} plus sign (+) or the word ON, and turned off
compiler directive instructs the compiler to link a when it is followed by a minus sign (-) or the
specified .res file into the resulting executable. By word OFF. In the case of the {$H} compiler direc-
default, every unit associated with a form includes at tive, it instructs the compiler to treat all string
least one {$R} compiler directive, which tells Delphi declarations as ANSIStrings when turned on, and
to include a binary version of the form’s .dfm file in as Pascal-style strings when turned off.
the compiled .dcu file as a Windows resource.
Parameter directives identify a file name, file
You can control compiler directives in one of two extension, or setting required by the compiler.
ways. The first way is to use the checkboxes on the The {$R *.RES} resource compiler directive is
Compiler page of the Project Options dialog box, as one example of a parameter directive. Condi-
shown in Figure 1. To see this page, select Project | tional directives permit you to instruct the com-
Options from Delphi’s main menu, and then select the piler to conditionally compile a segment of code.
Compiler tab. The {$IFDEF} directive is a conditional directive.

The second way is to embed compiler directives Compiler directives can also be categorized
directly in your code. Embedded compiler directives by their scope, which is either local or
look a lot like braced global. Global scope directives apply to the
comments. The only dif- compilation of the entire project. For example,
ference is that compiler the {$APPTYPE CONSOLE} directive instructs the
directives have a dollar sign compiler to build a console application. Local
($) immediately following scope directives, by comparison, apply to only
the opening brace. Like part of the code being compiled. For instance, a
all other statements inter- specific section of code can be compiled with the
preted by the compiler, {$H-} directive, instructing the compiler to treat
however, if the {$ com- all declared string variables as short strings in
bination appears within a that section, and with {$H+} for the remainder
comment, it’s ignored. of the project.

There are three types Every project has a default set of compiler direc-
of compiler directives: tives, defined by the values that appear on the
switch, parameter, and Compiler page of the Project Options dialog box.
conditional. Switch direc- If you like, you can instruct Delphi to explicitly
tives turn a particular fea- insert the current compiler directives into a unit.
Figure 1: Many of Delphi’s compiler options can be ture on or off. The {$H+} To do this, press COO (oh-oh, not zero-zero).
controlled using the Compiler page of the Project directive is a switch direc- An example of a unit where COO has been
Options dialog box. tive. Switch directives are pressed is shown in Figure 2.

13 June 2001 Delphi Informant Magazine


On Language

{$HINTS OFF}
function LaunchApp(const AppName: string): Boolean;
var
ProcessInfo: TProcessInformation;
StartUpInfo: TStartupInfo;
begin
Result := False;
try
FillMemory(@StartupInfo, SizeOf(StartupInfo), 0);
StartupInfo.cb := SizeOf(StartUpInfo);
Result := CreateProcess(nil, PChar(AppName), nil, nil,
False, NORMAL_PRIORITY_CLASS, nil, nil, StartUpInfo,
ProcessInfo);
finally
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
end;
Figure 2: Press COO in the editor to insert your project’s end;
{$HINTS ON}
default compiler directives at the top of the current unit.

As you inspect Figure 2, you will notice that the first compiler
directive includes a large number of switches. When you insert Figure 3: Turning compiler hints off for one method.
switch compiler directives manually, you can enter each separately,
each contained in its own set of braces, or include a comma-
separated list of the switch directives in a single set of braces.

Selected Compiler Directives


The following sections take a look at a sample of compiler direc-
tives, but because many of the compiler directives are self-explan-
atory, I will make no attempt to be comprehensive. Instead, I’ll
focus on those compiler directives that I find most interesting,
fun, or useful. For information on compiler directives not dis-
cussed here, you can press 1 on the Compiler page of the
Project Options dialog box, or select “compiler directives” from
Delphi’s online help index.

Suppressing Hints and Warnings


Delphi’s compiler will generate hints and warnings that can help you
write better code. For example, if you declare a local variable in a method,
Figure 4: Adding a resource script to a project in the Project
but never use it, the compiler will display a hint following compilation. It Manager inserts the enhanced compiler directive into your .dpr file.
will note potentially more troublesome problems with warnings.

Sometimes these generated hints and warnings are annoying. For the function sets the Result variable to False, the compiler will generate
example, you may have a function that really does return a value in a hint saying that the return value of the function may not be set. Since
every case, but the compiler cannot detect this. In these cases, you you know that the function always returns a value, you can turn off this
can turn hints and/or warnings off. annoying hint using the {$HINTS} compiler directive.

The {$HINTS} and {$WARNINGS} compiler directives are local switch Automatically Compiling Resource Files
directives. To turn hints off use: Until Delphi 4, the use of resource scripts to compile Windows
resource files required that you compile the resource scripts
{$HINTS OFF} manually using the Borland Resource Command-line Compiler
(BRCC32.EXE). Unfortunately, the manual nature of this compila-
To turn them back on use: tion made it possible for you to change a resource script, forget to
recompile it, and then link the old resource file into your application
{$HINTS ON} using the {$R} resource compiler directive.

For warnings use: A new version of the resource compiler directive was introduced in
Delphi 4. This version is shown in the following example, which
{$WARNINGS OFF} must be placed in your project’s .dpr file:

and: {$R 'filename.res' 'filename.rc'}

{$WARNINGS ON} When the Delphi compiler encounters this compiler directive, it
performs a number of valuable tasks. First it checks to see if the
Consider the function shown in the code segment in Figure 3. It named .res file exists. If it doesn’t, it automatically compiles the
simplifies the launching of an executable. Even though the first line of named .rc file to create the .res file. If the .res file already exists, the

14 June 2001 Delphi Informant Magazine


On Language

{$DEFINE USEINI} Setting the Executable File Extension


uses IniFiles, Registry; Under normal conditions, programs are compiled with the .exe
extension and libraries with the .dll extension. Using the exe-
procedure TForm1.Button1Click(Sender: TObject);
var
cutable extension parameter directive, however, you can instruct
{$IFDEF USEINI} Delphi to compile your executable with an alternative extension.
info: TIniFile; Normally, you will only use the executable extension directive with
{$ELSE}
libraries. This is because libraries can be loaded regardless of their
info: TRegistry;
{$ENDIF} file extension, while Windows will not know how to launch an
begin application unless it has an .exe extension.
{$IFDEF USEINI}
info := TIniFile.Create('INFO.INI');
{$ELSE}
Why, you might ask, would you ever consider changing the file
info := TRegistry.Create; extension of a library? One answer is that you might create a
{$ENDIF} resource-only DLL (one that contains no exported routines —
ShowMessage(info.ClassName);
end;
only Windows resources). If you want to distinguish these DLLs
{$UNDEF USEINI} from those that you export as executable subroutines, you can
provide it with an alternate extension. To set the file extension
Figure 5: Pseudo-code showing the use of {$DEFINE} and of a library, use:
{$IFDEF}.
{$E ext}
compiler compares the timestamps of
Defines Delphi
the .res and .rc files. If the .rc file is Version where ext is the file extension. For example, when Delphi generates a
more recent, it recompiles the .rc file library to define an ActiveX server, it uses the:
into a new .res file. As a result, when WIN16 16-bit
you link resources using this version WIN32 32-bit {$E OCX}
of the resource compiler directive, VER80 Delphi 1.0
you’re assured of always using the VER90 Delphi 2.0 directive to instruct the compiler to create a library with the OCX
latest version of your resource script. VER100 Delphi 3.0 extension. This trick is useful when you want to compile routines or
VER120 Delphi 4.0 resources into a DLL, but don’t want other developers to know what
Instead of manually adding this VER130 Delphi 5.0 type of file you used.
version of the resource compiler Figure 6: Common stan-
directive to your project source, dard conditional symbols. Conditional Compilation
you can use the Project Manager to You can instruct the compiler to conditionally compile one or
generate it. With your project open in the Project Manager, right- more lines of code using the {$IFDEF} and {$IFNDEF} compiler
click the node associated with your project and select Add. The directives. When you use these compiler directives, you follow
Project Manager responds by displaying the Add to project dialog them with a symbol name. If the symbol name is defined, the
box shown in Figure 4. statements following {$IFDEF} are compiled, and those following
{$IFNDEF} are not. You must include a corresponding {$ENDIF}
Set the File of type to Resource File (*.rc). Then select your resource for either the {$IFDEF} or {$IFNDEF} directives to indicate the
file and click the Open button. In response, Delphi will add the scope of the conditional compilation.
appropriate resource compiler directive to your project source.
Note: If you were using the older version of the resource compiler Symbol names are defined using the {$DEFINE} directive, which
directive, remove it before adding the newer version. If you fail to you follow with the symbol name you are defining. This symbol
remove the old directive after adding the new one, you’ll receive a name will remain defined for the remainder of the compilation, or
duplicate resource error when you try to compile. until the compiler encounters a $UNDEF compiler directive.

Controlling Optimizations The {$IFDEF} and {$IFNDEF} compiler directives can also be followed
Delphi’s compiler performs optimizations that produce significant by an {$ELSE} directive (but only before the corresponding {$ENDIF}).
increases in application performance. Sometimes, however, these This directive supplies the same if..then..else logic that you know from
optimizations can reduce your ability to debug your application. For Object Pascal. If the symbols are defined for an {$IFDEF}, the state-
example, it’s not uncommon for the value of many of your variables ments following {$IFDEF} are compiled and those following the nested
to be unavailable to the integrated debugger due to optimizations. {$ELSE} are not. The opposite is true concerning {$IFNDEF}. Figure 5
This can be annoying if these values are crucial to your debugging shows a pseudo-code segment that demonstrates the use of {$DEFINE}
process. (Note: Even when compiler optimizations are turned off, and {$IFDEF}.
some variable values can be unavailable to the integrated debugger.)
As the code appears here, clicking the button that uses this OnClick
The optimization compiler directive is a local directive. To turn event handler will result in the display of the message “TIniFile.”
optimizations off, use: If you comment out the {$DEFINE} directive and run it again, the
message will display “TRegistry.”
{$OPTIMIZATIONS OFF}
Standard Conditional Symbols
To turn them back on again, use: Delphi has a number of standard conditional symbols that it
defines, depending on the version of Delphi you’re running, and
{$OPTIMIZATIONS ON} under which operating system. You can use these symbols in the

15 June 2001 Delphi Informant Magazine


On Language

var counted. They are also very nice to use in most situations. How-
Form1: TForm1; ever, there are certain circumstances where you might want the
string type to refer to the original Pascal-style strings, which
{$DEFINE SPAN}
are arrays of Char of up to 255 characters in length. One such
{$IFDEF ENGL} situation is when you have written an application that you must
{$INCLUDE 'englstrs.txt'} continue to compile in both 16-bit and 32-bit versions of Delphi,
{$ELSE}
and you want to treat your strings the same regardless of the
{$IFDEF SPAN}
{$INCLUDE 'spanstrs.txt'} version. (Granted, this situation was more common a few years
{$ENDIF} ago, but there are some Delphi developers who must support both
{$ENDIF} 16-bit and 32-bit applications.)
implementation
... While the individual characters in both ANSIStrings and Pascal
strings can be indexed, only the Pascal-style strings include a zero-
Figure 7: This code shows a resourcestring block. order byte, the Ord value of this character indicating the length of
the string. ANSIStrings don’t have a zero-order byte. If you have
{$IFDEF} and {&IFNDEF} compiler directives without using a cor- an application that expects a string to include a zero-order byte,
responding {$DEFINE} directive. Figure 6 shows a table of the regardless of which version of Delphi is compiling the application,
most common of these conditional symbols, and the version of you can use the {$H OFF} compiler directive.
Delphi with which they’re compiled.
Note, however, that while Delphi 2 and later recognize the {$H}
Delphi 6 and Kylix each will include one new standard condi- directive, Delphi 1.0 (the 16-bit version) does not. Consequently, the
tional symbol. When compiling a project using Kylix, the LINUX {$H} compiler directive has to be made conditional, thus:
symbol will be defined. By comparison, when compiling with
Delphi 6 and later, the WINDOWS symbol will be defined. These {$IFDEF WIN32}
defines will be valuable when you create projects that you want to {$H OFF}
{$ENDIF}
be able to compile for both Linux and Windows.

Use of the WIN32 standard define is demonstrated in the following Conclusion


section. Compiler directives permit you to control the actions of the
compiler. By familiarizing yourself with the available compiler
Inserting Code into a Unit directives you can make certain that you are getting the most out
Use the {$INCLUDE} compiler directive (or simply {$I}) to insert of Delphi. ∆
a segment of code at the location of the directive. This code is
compiled as though it were originally part of the unit in which
the directive appears.

One valuable use of the {$I} compiler directive is to conditionally


include one of several possible resourcestring blocks in a file
(resourcestring blocks define constant-like symbols that are stored
in the executable as string resources). For example, imagine you
have three files, each containing a resourcestring block that defines
resource strings in a particular language. The code in Figure 7 dem-
onstrates this use, depending on the presence of a defined symbol.

The following is an example of what the ENGLSTRS.TXT file


might contain:

resourcestring
sMainFormCaption = 'Example Project';
sOKButtonCaption = '&OK';
sCancelButtonCaption = '&Cancel';
sFileExit = '&Exit';
sFileOpen = '&Open'
sFileClose = '&Close';
sFileReport = 'Re&port';
// More resourcestring declarations...

After processing the {$INCLUDE} compiler directive, the contents of Cary Jensen is president of Jensen Data Systems, Inc., a Houston-based database
the appropriate .txt file are compiled along with all other code within development company. Cary is co-author of 17 books, including Oracle JDeveloper
the unit. [Oracle Press, 1998], JBuilder Essentials [Osborne/McGraw-Hill, 1998], and
Delphi in Depth [Osborne/McGraw-Hill, 1996]. He is a Contributing Editor of
Using Short Strings by Default Delphi Informant Magazine, and an internationally respected trainer of Delphi and
Delphi’s ANSIString type is the default type for 32-bit applica- Java. For more information, visit http://www.jensendatasystems.com, or e-mail
tions. These strings are large, dynamically allocated, and reference Cary at cjensen@compuserve.com.

16 June 2001 Delphi Informant Magazine


Sound+Vision
OpenGL / 3D Graphics / Delphi 4, 5

By Eli Bar-Yosef

Getting Started with OpenGL


Part II: Lighting, Materials, and Texture

L ast month’s article introduced OpenGL programming to Delphi developers. You


learned how to construct objects, set their colors, and transform them. Now it’s
time to light them up. Lighting is a vital part of 3D programming; without lighting
effects, 3D objects just don’t look like 3D objects.

Lighting Diffuse light comes from a specific light source (see


The problem with lighting is that there is no static Figure 2). When it hits a surface, however, it spreads
formula for achieving results. You have to experi- in all directions lighting an object or scene uniformly.
ment until you get the desired effect. Lighting is
more of an art than a science. And lighting is a Specular light also comes from a specific light
huge topic; there’s no way to do it justice in a source, but is more focused (see Figure 3). When
single article. With this in mind, let me attempt specular light hits a surface it’s reflected in a
to provide a starting point — an approach to the specific direction. A good example is the shine on
topic that you can take further if you wish. an apple, or the gleam in an eye.

The OpenGL lighting model is divided into three To create light sources, use the following function:
lighting types: ambient, diffuse, and specular. Each is
computed separately, then combined. Ambient light procedure glLightf*(light: GLenum,
is light that has bounced and spread in so many pname: GLenum, param: Glfloat)

directions that it doesn’t have a specific source (see


Figure 1). When this kind of light hits a surface, it’s (Note: In this series, an asterisk is used to
not focused on a specific point; it spreads everywhere represent OpenGL routines with multiple endings,
in a scene. e.g. glColor* represents glColor3b, glColor3bv,
glColor3d, etc.) As you can see, this function
requires three parameters. The first is the light
source name. When you create the light parameter,
name it GL_LIGHTi, where i should be greater
than zero, but less than GL_MAX_LIGHT. The
value of GL_MAX_LIGHT depends on your
specific implementation, but according to the
OpenGL specification, you should be able to create
at least eight light sources.

The second parameter, pname, describes the char-


acteristics of the light source: GL_AMBIENT,
GL_DIFFUSE, GL_SPECULAR, GL_POSITION,
etc. There are more values for achieving different
Figure 1: Ambient light. effects, e.g. spotlighting or setting the light attenu-

17 June 2001 Delphi Informant Magazine


Sound+Vision
make objects look real. For instance, let’s say you want to build a
box that appears to be made of stone. Building a simple cube and
adding some fancy colors won’t get you there; it will still look like
a decorative cube.

To really achieve the effect, you have to use texture by adding a


bitmap that looks like stone. OpenGL has a set of functions that can
help you to use texture in your scene. The first is glGenTextures:

glGenTextures(n: Integer; TextureNames: array of Integer)

This call should name each of the textured objects with a unique
name. The first parameter takes the number of textured objects you
want to name; the second takes an array of where you would like
to place the object.
Figure 2: Diffuse light.
Another useful routine is the glBindTexture procedure:

ation factor. Check out the OpenGL API documentation for more glBindTexture(target: Glenum; textureName: GLuint)
information.
It creates a new textured object and assigns it a unique texture name
The last parameter is a bit involved. In general, it sets properties of that you generate using glGenTextures. The function can also be used
the previous parameter, pname, and its value and format depend on to activate an old textured object. After generating a texture name
the value of pname. and assigning an object to it, we add our texture image to it. This can
be done by using the procedure glTexImage*:
Materials
Objects in the real world are made from specific materials with unique procedure glTexImage*(target Glenum;
reflective properties. One material might reflect light, another might Level: GLint ;
Components:GLint ;
absorb it, and so on. Why can’t we specify the material of the object
Width: GLsizei ;
that we construct? We can. You might have noticed from the previous Height: GLsizei ;
example that materials go hand in hand with lighting. To emphasize Border: GLuint ;
this point, let’s look at how this is calculated. Let’s say you have a light Format: GLEnum ;
source, L1, and an object with material of M1. The calculation is: Type: GLenum ;
Pixels: Pointer);

(L1R*M1R, L1G*M1G, L1B*M1*B)


This function has three versions (glTexImage1D, glTexImage2D,
You can see from the calculation that the material might reflect a and glTexImage3D), although in this article we will concentrate
certain color and absorb another. To specify the object’s material, on 2D. GL_Texture_2D is passed as the first parameter (check
you can use the function glMaterial*. Its parameters are similar the OpenGL API for more options) to specify that we want to
to those of the glLight* function. The first parameter is the use 2D texture. The second parameter is the mipmap Level of
face of the object, i.e. on which face of the object the material Detail (LOD). We’re not dealing with mipmaps in this article,
should be used. Available values are: GL_BACK, GL_FRONT, and so set it to zero. The third is the number of colors and is set to
GL_FRONT_AND_BACK. GL_RGB. The fourth and fifth parameters are the height and the
width of the texture image, and must have the size of 2^x and
The second parameter is the same as with the glLight* function; you a minimum size of 64x64. The sixth parameter is the border of
specify how the object should react to a specific light type. Possible the image, and it can be set to 0 for no border, or to 1 for a
values include: GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, border. The format and type of parameters tell OpenGL about
GL_AMBIENT_AND_DIFFUSE, the format and type of the pixel data. The last parameter is the
and GL_EMISSION. Most texture data.
should be familiar to you
except for GL_EMISSION, Now that we have the texture image, we need to specify the filtering
which makes an object appear method. In the simplest sense, filtering is a way to specify how the
to emit light. texture image will look on the screen. This is done by using the
glTexParameter* procedure:
As with the glLight* function,
the third parameter modifies glTexParameter*(target: GLEnum; pname: Glenum;
the characteristics of the previ- param: GLfloat)

ous parameter.
This function is usually called twice: once for texture image
Texture magnification, and the second time for texture image reduction.
Texture is very important and The first parameter is the target, as with the previous
you’ll employ it in nearly every function. The second is GL_TEXTURE_MAG_FILTER or
Figure 3: Specular light. 3D application. Texture helps GL_TEXTURE_MIN_FILTER (magnification or reduction).

18 June 2001 Delphi Informant Magazine


Sound+Vision
The third is used to specify the kind of filter to use. In other
words, you must specify if you want speed or quality. When
you want great picture quality and speed is not an issue, use
GL_LINEAR filter, and vice versa. Filtering is all about give
and take.

The last routine for this section is glTexCoord*. This function is used
to map the texture image on the object coordinates. The parameters
of this function depend on the object vertex. To explain this function,
let’s say we have a 2D quad that needs to be textured. This means we
have four vertexes we should handle, so we need to call glTexCoord2D
four times. The parameters of glTexCoord2D are supposed to be filled
with the location of the vertexes.

The first parameter indicates if the vertex is on the right side. If so,
this parameter is 1; otherwise it’s 0. The second indicates whether
the vertex is a top vertex: if so, it must contain 1; otherwise it’s 0. Figure 4: “The Scene;” the demonstration application in action.
Here’s a quick example for clarification; we’ll use the polygon we
built in Part I:
The parameters of this structure, nSize, should hold the size of
glBegin(GL_POLYGON); the structure. The nVersion is the version and must always be set
glTexCoord2D(0.0, 0.0) to 1. The dwFlags is set to support PFD_SUPPORT_OPENGL
glVertex3f(-1.0, -1.0 ,0.0);
and PFD_DOUBLEBUFFER. The PFD_DOUBLEBUFFER is
glTexCoord2D(1.0, 0.0)
glVertex3f(1.0, -1.0, 0.0); included because it helps eliminate animation flickers. The last
glTexCoord2D(1.0, 1.0) flag, PFD_DRAW_TO_WINDOW, is used to allow the buffer to
glVertex3f(0.5, 0.5, 0.0); draw directly to the window. Then we set the color bits and the
glTexCoord2D(0.0, 1.0) depth bits both to 24. This way every color buffer will have 2^24
glVertex3f(-0.5, 0.5, 0.0);
glEnd();
bits of color. iLayerType is set to GL_MAIN_PLANE. The rest
are all set to zero.

Now that you’re familiar with the basic concepts and commands, it’s It’s then necessary to retrieve the window DC using the GetDC func-
time to start programming your first OpenGL program. tion. This has to be done to allow the use of the two functions that
help to choose and set our pixel format. Next, the ChoosePixelFormat
The Scene function checks if Windows can serve our pixel format request. If it
This simple demonstration will show the implementation side of can, it will return the non-zero pixel index.
the issues described in this series (see Figure 4). You don’t need
to worry about the installation of special libraries to run the If we have a valid index, we can use SetPixelFormat to safely set the
demonstration. All the libraries used in this demonstration come current pixel format of the window to fit our needs. Then we must
with the Windows operation system. There’s plenty of informa- create our RC using wglCreateContext. This function takes the window
tion to help you understand the framework of this demonstration DC as an argument. After we successfully create the RC, we need to
and the role of every method in it. For an in-depth understand- make it the current RC by using glMakeCurrent. This function takes
ing of the code, you should explore the code. The complete the current DC so that future calls to OpenGL will eventually draw
demonstration application is available for download; see end of on the DC. The second parameter is the RC we’ve created.
article for details.
Since we have finished handling the RC, we can start program-
Setting the OpenGL Window ming our scene. Note that when you close the program you
Normally we get a full-fledged form and start working. Unfortunately, must release the DC and the RC. There is not really that much
you will have to build your own window when you work with OpenGL. to explain; all of the commands are the reverse of the previous
More specifically, you can manage the form to collaborate with OpenGL. ones. (You can see the code under the WM_CLOSE event in the
This can add significant overhead to your application, however, and WinMain function.) Now that you know how to create a suitable
acceptable speed is a crucial aspect of a good 3D application. window for OpenGL rendering, let’s explore the classes used to
implement the demonstration application.
You can see from the code that the Win32 API was used directly
to construct the window. Building the window is outside the scope The Scene’s Objects
of this article, so let’s start with setting the OpenGL rendering The scene is built from 10 classes, each of which has special
context (RC). characteristics:

The OpenGL RC is similar to the window device context (DC). Particle system. As you can see in the demonstration, particles are
All OpenGL 3D operations are executed on this RC. You can spread all over the scene. This kind of effect is called a particle
see in the WinMain function (under the WM_CREATE event) system. The particle system implemented in this demonstration
all the operations that require building the RC. Using the consists of three classes from which the other classes are derived.
PIXELFORMATDESCRIPTOR structure does some of the work; We’ll look at them using a top-down approach, starting with the
it’s filled with information to represent our request. TParticle class and ending with TParticleSystem.

19 June 2001 Delphi Informant Magazine


Sound+Vision
TParticle. This class describes a particle’s properties: Texture (TDIB, or device-independent bitmap). This class is used to
The x/y/z gravity group holds the gravity of each particle. extract useful information (needed by our TTextureLoader) from the
The x/y/z direction group holds the direction of the particle. given texture image:
The x/y/z group holds the final location of the particle, i.e. the OpenImageFile opens the image file given as a parameter.
combination of gravity and direction. IsBitmap checks the given image file for validity as a bitmap.
The red/green/blue group holds the color of each particle. ReadImageDimensions extracts the width and height of the given
The TTL property holds the time-to-life value. Eventually, when texture image.
this property gets to zero, the particle will die and will be initial- ReadImageData extracts the data image.
ized to 2.0. ExtractInfo combines the above functions.
The PrevParticle property holds the previous particle in the list
for iterative purposes. TTextureLoader. This class is derived from TDIB and makes the
The NextParticle property holds the next particle in the list for supplied texture images useable in the 3D scene. There is only one
iterative purposes. method that should be called, LoadTexture. By using this method, it’s
possible to set the image to be usable on the 3D objects.
TParticleListHandler. This class consists of methods and properties
for maintaining the particle list: TTextureControl. This class is used for dynamically allocating texture
RemoveParticle removes a specific particle from the list. objects, and to hold a reference to the texture object used by glBind:
CleanParticleList destroys all particles on the list. SetTexture builds a TTextureLoader object and is used for loading
AddParticle adds particles to the list. the texture. It also holds the reference to the texture object in a
ParticleNumber contains the number of particles in the list. private data member, m_auiTexture.
FirstParticle points to the first particle in the list. TextureID is an interface to m_auiTexture.

TParticleSystem. The methods in this class handle the motion and TSceneBuilder. This class is derived from TSceneHandler and its
rendering of particles on the screen: aggregate, TSceneObjects, and is used for building the scene. This class
RenderParticle renders the particle on the screen. has just one method, named DrawScene. This method is virtual and
GenerateTTL generates a random TTL for every particle. abstract, and is being implemented in the TSceneBuilder class.
SetParticleDirection sets the particle direction using an equation
that generates a random screen location. This has been a very brief introduction of the classes, so I highly
SetParticleGravity sets the particle gravity. recommended you explore the demonstration application to get a
UpdateParticle includes all of the above to create motion. full understanding of the classes and how they interact to create
the scene.
TRingData. If you take a quick look at the demonstration, you will
notice several rings that rotate around a cube. TRingData is used as Conclusion
a container to hold the ring data. The x/y/z properties contain the We’ve powered through some core ideas concerning one of the more
location of each element in the ring. important APIs in the 3D industry. Although we haven’t covered the
more advanced facets of 3D graphics and OpenGL, the information
TSceneOjects. This class is derived from TParticleSystem and contains in this article should help you embark on an OpenGL education. ∆
all the 3D objects in the scene:
InitRing generates the ring data. The complete demonstration project referenced in this article is available
DrawFirstRing, DrawSecondRing, DrawThirdRing, and on the Delphi Informant Magazine Complete Works CD located in
DrawFourthRing draw the rings to the screen and set the INFORM\2001\JUN\DI200106EB.
rings’ materials.
DrawFloor draws the floor (better described as a net) of the scene.
DrawCube draws a 3D cube, setting its materials and texture.
RingData is an interface to all the ring data elements.
SetObjectProperties sets the object properties, e.g. for line smooth-
ing and setting the shading model.

TSceneHandler. This class is responsible for handling the scene,


including keyboard, buffers, and lights:
InitLight “ignites” the scene and sets the light properties.
EnableBuffers enables the vital scene buffers. The demonstration
is relatively simple, so we only need to enable the depth buffer.
SceneResize sets the perspective projection of the scene, then sets
the current matrix to the ModelView matrix.
SceneSpecialKeyboard is called for the WM_KEYPRESS message
and handles the virtual keys. Eli Bar-Yosef is a professional software developer in Israel. He mainly uses
SceneKeyboard is called for the WM_CHAR message to handle C/C++ and Delphi, and specializes in Windows internals, networking, and
non-virtual keys. COM/DCOM. Currently, he is working on several projects as a freelancer, and
InitScene combines some of the methods described above and on a new and special project for which he is seeking funds. In his free time,
calls them. he enjoys programming real-time 3D demos and reading. If you have any com-
DrawSceneObjects is a virtual abstract method that must be ments, suggestions, or if you want information about this project, contact him at
implemented in TSceneBuilder. eli_by@netvision.net.il.

20 June 2001 Delphi Informant Magazine


Kylix Quickstart
Linux / X Window System / Kylix

By Charlie Calvert

Kylix and Xlib


A “Hello World” X Program from Scratch

T his is the second in a series of articles on Kylix and the Linux programming
environment. In the April issue of Delphi Informant Magazine you were introduced
to the Linux desktop, and became acquainted with some general facts about the world
of the X Window System, or more simply, X.

This second article in the series is relatively techni- Figure 1). The second is a standard Kylix applica-
cal, and the good times and light-hearted jokes are tion that queries X to find out about the attri-
few and far between. For those desperate for more butes of the current display (see Figure 2). The
accessible information, I’ll start by recommending purpose of this program is to show you how
a few books that all Linux users absolutely need to the low-level code found in this article relates to
own. These basic books describe in glorious detail standard CLX programming. A final program is
how to use the environment. a bit more complete, and offers a more in-depth
survey of what you can do when programming
If these were Windows books, you might fear they X. It is shown in Figure 6 on page 24, and
were trivial. “At the bottom of the screen, you will is available for download (see end of article for
see the Start button. Hold the mouse firmly in the details). This latter program gives you the ability
palm of your hand and slide it....” Okay, enough to handle keyboard input.
of that. Linux, fortunately, is a technical world, so
even the basic books on using the environment are The first two programs do little more than pop
filled with arcane information. Here are two that up windows and draw a bit of text in them. The
contain most of the lore you must master to be on second program, however, gives you a few ideas of
speaking terms with your Linux box: A Practical how to access some of the special features of the
Guide to Linux by Mark Sobell [Addison-Wesley, X programming environment. (Unlike the corre-
1997], and Running Linux, Third sponding situation in Windows, you can of course
Edition by Matt Welsh, et al. find the entire source for Xlib. The best place to
[O’Reilly & Associates, 1999]. look for it is at http://www.xfree86.org.)

If you have no flesh and blood The low-level X programs shown in this article,
guru to which you can turn, I and the next in this series, are extremely atypical
suggest you purchase both books. examples of how to program with Kylix. They’re
When you’re getting started with fairly complex in structure, and don’t use any of
Linux, a second opinion can be the helpful tools and objects usually employed
invaluable. in a Kylix program. Writing the same kind of
program using the Kylix visual tools and CLX
Programming X with Kylix would be trivial in the extreme. It would, at
In this article, I’ll show you most, take you about two minutes to complete.
three X programs. The first is a However, if you don’t use CLX, and instead
bare bones program that shows write directly to the X library API, then the task
Figure 1: A bare bones “Hello World” pro- you the basics of directly pro- of creating a simple GUI program is consider-
gram written using Xlib. gramming X from Kylix (see ably more difficult.

21 June 2001 Delphi Informant Magazine


Kylix Quickstart

PDisplay = ^Display;
Display = record
ext_data : PXExtData; // Hook for extension to hang data.
private1 : PXPrivate;
fd : Longint; // Network socket.
private2 : Longint;
proto_major_version : Longint;
proto_minor_version : Longint;
vendor : PChar; // Vendor of the server hardware.
private3 : XID;
private4 : XID;
private5 : XID;
private6 : Longint;
resource_alloc : TDisplayResourceAllocProc;
byte_order : Longint; // Screen byte order.
bitmap_unit : Longint; // Padding and data requirements.
bitmap_pad : Longint; // Padding requirements on bitmaps.
bitmap_bit_order : Longint; // Least or MostSignificant.
nformats : Longint; // Number of pixmap formats in list.
pixmap_format : PScreenFormat; // Pixmap format list.
private8 : Longint;
release : Longint; // Release of the server.
private9 : PXPrivate;
Figure 2: A somewhat more complex Xlib program that uses bit- private10 : PXPrivate;
maps, fonts, and communicates properly with the window manager. qlen : Longint; // Length of input event queue.
last_request_read : Cardinal; // Seq num last event read.
request : Cardinal; // Sequence number of last request.
Just to set the record straight, let me make it clear why I’m showing private11 : XPointer;
you these more complex programs: First, they help you understand X private12 : XPointer;
and how it works. Second, they show that Kylix programmers have private13 : XPointer;
access to this kind of low-level functionality if they need it. Xlib is private14 : XPointer;
max_request_size : Cardinal; // Max words in request.
a very technical API, and being able to access it directly is one of
db : PXrmHashBucketRec;
the features that helps to set Kylix apart from other programming private15 : TDisplayPrivate15Proc;
languages. Usually Xlib is available only to C and C++ programmers. display_name : PChar; // "host:display" string.
Kylix’s ability to execute a line-by-line translation of an Xlib program default_screen : Longint; // Default screen.
written in C helps to show the power of the Object Pascal language. nscreens : Longint; // Number of screens on this server.
screens : PScreen; // Pointer to list of screens.
motion_buffer : Cardinal; // Size of motion buffer.
Finally, if you understand how X works, then you can get a feeling for private16 : Cardinal;
the capabilities and limitations of a standard Kylix program. All Kylix min_keycode : Longint; // Minimum defined keycode.
programs are built on top of X, and if you understand how X works, max_keycode : Longint; // Maximum defined keycode.
private17 : XPointer;
then you know what you can and cannot do with Kylix. In general, if
private18 : XPointer;
you can do it in X, then you can do it in Kylix. The point I’m trying private19 : Longint;
to make here should be self evident to all experienced programmers. xdefaults : PChar; // Contents of defaults from server.
In particular, if you know the low-level APIs for a process, then your // There's more, but it's private to Xlib.
ability to use a wrapper around that API is significantly improved. end;

In a sense, both Qt and CLX are really nothing more than wrappers
around X. As a result, it pays to know a little about how X works. Figure 3: The Display record.

Getting Documentation on X
Before I show you the X program, I want to take a moment to discuss If you are running Red Hat, Mandrake, or many other standard
getting documentation on programming in X. Once or twice in my distributions, you can get the online docs for XFree86 by download-
text I will tell you to get additional information by consulting info or ing a file with a name like XFree86-devel-4.0.1-1.rpm. Remember
man. If I’m going to do that, I have to be sure you can actually find that you can find the current version number of X on your Red
the references to which I allude. Hence the need for this section. Hat system by typing rpm -q XFree86. If you want the version of
your X documentation, type rpm -q XFree86-devel. If the files are
To begin, I need to say a few words about the program called RPM, available on your system, pass rpm -qd to find the directories where
the Red Hat package manager. It’s the tool you use on many Linux the docs are stored, or -qi to get general information on the release.
distributions when you want to install or update a new program. For instance, type:
You are about to get the 30 second course on the subject. For more
information, see the books I mentioned earlier, or type man rpm at rpm -qi XFree86-devel
the Linux shell prompt.
Finally, you can pass in rpm -ql, where l stands for list. This will
To install a new package, go to the command prompt and type give you a list of files in the package.
rpm -i mypackage.rpm. To upgrade a package, type rpm -Uhv
mypackage.rpm at the command prompt. To delete or erase a pack- In general, you’ll know if the development docs are installed correctly
age, type rpm -e mypackage.rpm. There you have it — the world of if man gives you an entry when you type man XCreateWindow. If man
RPM in 30 seconds. Phew! Now let’s apply what we’ve learned about tells you that it knows nothing about XCreateWindow, then XFree86-
RPM to the world of Xlib programming. devel is either not installed on your system, or not installed correctly.

22 June 2001 Delphi Informant Magazine


Kylix Quickstart

program xdpyinfoGUI; unit Main;

uses interface
QForms,
Main in 'Main.pas' { Form1 }; uses
SysUtils, Types, Classes, QGraphics, QControls, QForms,
{$R *.res} QDialogs, QStdCtrls;

begin type
Application.Initialize; TForm1 = class(TForm)
Application.CreateForm(TForm1, Form1); Button1: TButton;
Application.Run; ListBox1: TListBox;
end. procedure Button1Click(Sender: TObject);
end;

Figure 4: The .dpr file for the xdpyinfoGUI program initializes the
var
Kylix libraries and launches the main form of the program. Form1: TForm1;

implementation
Further documentation for X isn’t scarce. You can expand your
uses
horizons by going to http://www.xfree86.org/support.html, where
Xlib;
you’ll find detailed documentation, and an entire book on XFree86.
You might also wish to purchase the Xlib Programming Manual, {$R *.xfm}
Volume One, by Adrian Nye [O’Reilly & Associates, 1992], the first
in a series of technical books on this subject. procedure TForm1.Button1Click(Sender: TObject);
var
display: PDisplay;
A “Hello World” Program in X begin
This first program makes an end run around a number of the more display := PDisplay(Application.Display);
complex features of an X program. As a result, it’s relatively short, ListBox1.Items.Add('Display version: ' +
IntToStr(display.proto_major_version) + '.' +
weighing in at under 100 lines. It’s shown in Listing One (on page
IntToStr(display.proto_minor_version));
25). Note: The hundred lines of code in this program must be ListBox1.Items.Add('Release number: ' +
typed in nearly entirely by hand (if you don’t just download it). IntToStr(display.release));
The Kylix IDE will not generate more than the sparest outline of ListBox1.Items.Add('Vendor: ' + display.vendor);
the program for you. If you built the same program in Kylix using ListBox1.Items.Add('Display name: ' +
display.display_name);
CLX and the visual tools, however, the total number of lines would ListBox1.Items.Add('Network socket: ' +
be much fewer, and you would have to do almost no typing to IntToStr(display.fd));
create it. As I said, this is an extremely atypical Kylix program. ListBox1.Items.Add('Default screen: ' +
IntToStr(display.default_screen));
ListBox1.Items.Add('Number of screens: ' +
The program follows the basic structure of a simple console applica-
IntToStr(display.nscreens));
tion, though of course it runs in a window. By saying this, I mean ListBox1.Items.Add('Screen width: ' +
that it’s based on the following very simple program structure: IntToStr(display.screens.width));
ListBox1.Items.Add('Screen height: ' +
program simple; IntToStr(display.screens.height));
begin end;
end.
end.

The line of code that says begin is the entry point for the program,
Figure 5: The main form of the xdpyinfoGUI program presents
just as main is the entry point for a C program. Execution stops the user with information about the first X display on the system.
when the code reaches the line of code that reads end. The core of
the code in the program appears between the begin and end pairs
that represent the main block of the program. Additional functions functions at the command line by typing info FunctionName. For
and procedures can be added between the program statement and instance, typing info XDrawString at the shell prompt should get
the begin line. you Linux-based help on the XDrawString function. You can also
type man FunctionName to get help. If these commands don’t work
The program relies on a big unit named Xlib that contains defini- for you, then you either don’t have info or man installed, or you
tions of the structures, constants, and functions found in the Xlib don’t have the docs for Xlib installed. In either case, you should have
binary libraries. These libraries are, of course, written in C. As the RPM for these files available on your system.
I stated earlier, Kylix can have trouble calling libraries written in
C++. However, it can call functions written in C. There is no Obtaining the Display
significant difference in performance when calling a function writ- The first significant call in the Simple2 program looks like this:
ten in C from Object Pascal or from C. In other words, this X
program will have essentially the same performance characteristics display_name := XDisplayName(display_name);
as the same program written in C.
This function returns the name of the current display. Usually this
Most of the functions in this program are part of Xlib. Very few calls name is :0:0. (Additional information on displays and display names
are made to native Object Pascal functions. You can get help on X is presented in the previous article, in the section called “Attaching to

23 June 2001 Delphi Informant Magazine


Kylix Quickstart
This is a good time to demonstrate how this kind of information
applies to a standard Kylix program that uses the Qt library. The
code in Figures 4 and 5 define such a program. As you can see from
looking at Figure 6, this program shows the user information about
the current X display.

To create this simple program, open Kylix and choose File | New
Application. Drop a button and list box on the main form of the
program. Double-click on the button to create the Button1Click
method. Add the code for the method shown in Figure 5. Finally, add
the Xlib unit to the uses clause.

Note the code giving you access to the Display record talked about
throughout this section of the text. All Kylix programs initialize a
global Application object created automatically at program start up.
You can view part of this initialization process in Figure 4.

If you right-click on the Application.Display variable in the Kylix


Figure 6: The output from xdpyinfoGUI gives the user insight into
editor, and choose Find Declaration from the popup menu, you will
the status of the first X display on the system.
be taken to the declaration for this object. You’ll see the field is a
property of type PDisplay. In the shipping version of the product, this
a Remote Server.”) Once you have the name of the display, you can should be of the same type as the PDisplay variable whose lengthy
open an instance of it and retrieve a handle to it: declaration I have just shown you. Unfortunately, in the beta version
I’m working with, it’s declared as a pointer, and must be typecast to a
display := XOpenDisplay(display_name); variable of type Xlib.PDisplay thus:
if display = nil then begin
WriteLn('Could not open display: ', display_name);
var
Exit;
display: PDisplay;
end;
...
begin
You can see that the program uses WriteLn to output error or status display := PDisplay(Application.Display);
messages. These messages don’t appear in your window, but rather
in the command window from which you started the program. If
you started Kylix from the desktop, then you might not see these You now hold a pointer to a record. The record is of the same type
messages. As a result, I often start Kylix from a command prompt, found in the Simple2 program shown in Listing One on page 25.
using the string startkylix. In particular, this is the same type of variable that XOpenDisplay
returns. XOpenDisplay is an Xlib call, of the kind a C or C++ pro-
XOpenDisplay returns a large and complex record named Display, as grammer would make. Application.Display is a CLX property of
shown in Figure 3. Notice the first two lines of this declaration: the kind accessed by the average Kylix programmer. In the normal
course of things, neither a Java nor a Visual Basic programmer
PDisplay = ^Display; would ever get access to a low-level system variable of this type.
Display = record Even if they did get access to it, their language isn’t designed
to allow them to manipulate it properly. In particular, Java and
A Pascal record is the same thing that a C programmer would call Visual Basic make it difficult for programmers to work with
a struct. A Java programmer would call a record a class that has pointers. Object Pascal gives you full access to low-level system
no methods, but only data and pointers to functions. In particular, functionality, while wrapping that access inside a language is
a record differs from a class in that it doesn’t have constructors or generally no more difficult to use than Java or Visual Basic.
destructors, and doesn’t support inheritance or polymorphism. The
declaration shown here says that type Display is a record containing Once you have obtained the Display variable, the body of the
a series of fields, such as fd, which is of type Longint. Other fields, Button1Click method itself is straightforward. It simply accesses
such as PXExtData, are themselves of type record, and represent features of the Display record to create a report for the user. For
nested records. better or worse, the most recent versions of Object Pascal allow
you to access pointers with dot notation or pointer notation, so
These first two statements indicate that PDisplay is a pointer to a you can access the fields of the object in two ways:
record of type Display. In Object Pascal, pointers almost always begin
with the letter P. A standard non-pointer type would usually start display.screens.height
with a T, as in TDisplay. The absence of the T in this particular case is display^.screens^.height

a bit of an anomaly, and probably results from the translator’s attempt


to give the Xlib unit a flavor of the X Library. One last point should be made about the Display variable before
moving on. In some cases you might want to call XSynchronize
If you recall the xdpyinfo utility mentioned in the section on remote immediately after getting the display. This can help when you are
displays, then the body of this structure might begin to come into debugging:
focus. In particular, most of the information available in xdpyinfo
appears to be derived from this structure. XSynchronize(display, 1);

24 June 2001 Delphi Informant Magazine


Kylix Quickstart
Conclusion
That’s all I have room for this month. In the next article, we’ll continue Begin Listing One — A simple X program
this exploration of Kylix and X-servers. Because this article has been program Simple2;
{ Demo of basic Xlib code with very few bells and whistles.
unusually technical, it might help to end with a few useful tips
Copyright (c) 2001 by Charlie Calvert. }
that all Kylix programmers can appreciate. If you’re new to Linux, uses
you might not yet know some of the key Linux sites on the Web. Xlib, SysUtils;
Here are few you should visit regularly if you want to keep up with var
Display: PDisplay;
all things Linux: http://www.linux.com, http://slashdot.org/index.pl, Window: TWindow;
http://freshmeat.net, http://linuxgames.com, http://osdn.com, http:// gc: TGC;
www.linuxworld.com, and http://www.gnu.org. Of course, one very procedure DrawMyString(MyString: string);
var
important site is the Linux Document Project at http://www.ibiblio.org/ len: Integer;
mdw/index.html. ∆ begin
len := Length(MyString);
XClearWindow(display, window);
This article is adapted from material for the upcoming Kylix Developer’s
WriteLn('drawing MyString of length ', len);
Guide by Charlie Calvert, Margie Calvert, David Intersimone, and John XDrawString(display, window, gc, 10, 100,
Kaster. It’s due in July 2001 from SAMS; ISBN: 0672320606. PChar(MyString), len)
end;

The files referenced in this article are available on the Delphi var
Informant Magazine Complete Works CD located in INFORM\2001\ xgcv: XGCValues;
JUN\DI200106CC. attributes: XSetWindowAttributes;
event: XEvent;
attributes_mask: Integer;
display_name: PChar;
done: Integer;
begin
display_name := nil;
display_name := XDisplayName(display_name);
display := XOpenDisplay(display_name);
if display = nil then begin
WriteLn('Could not open display: ', display_name);
Exit;
end;
WriteLn('Cuccessfully opened display ', display_name);
XSynchronize(display, 1);
attributes.event_mask := KeyPressMask or
KeyReleaseMask or ButtonPressMask or
ButtonReleaseMask or ExposureMask;
attributes.background_pixel :=
XBlackPixel(display, XDefaultScreen(display));
attributes.border_pixel :=
XBlackPixel(display, XDefaultScreen(display));
attributes_mask := CWBackPixel or
CWBorderPixel or CWEventMask;
window := XcreateWindow(display,
XDefaultRootWindow(display), 0, 0, 300, 300, 0,
XDefaultDepth(display, XDefaultScreen(display)),
InputOutput,
XDefaultVisual(display, XDefaultScreen(display)),
attributes_mask, @attributes);
WriteLn('Successfully created window\n');
XMapWindow(display, window);
WriteLn('Successfully mapped windows\n');
WriteLn('Attempting graphics context creation\n');
xgcv.foreground := $00FFFFFF;
xgcv.background := $00000000;
gc := XCreateGC(display, window,
(GCForeground or GCBackground), @xgcv);
WriteLn('Successfully created graphics context');
done := 0;
while done <> 1 do begin
XNextEvent(display, @event);
case (event.xtype) of
Expose:
begin
if event.xexpose.count <> 0 then
Break;
DrawMyString('The wondrous world of X!');
end;
KeyPress: done := 1;
ButtonPress: ;
end;
end;
Charlie Calvert is the author of eight books, including the best-selling Delphi XCloseDisplay(display);
Unleashed series. A speaker at eight Borland conferences, Charlie has also given end.
courses at many other sites around the globe. End Listing One

25 June 2001 Delphi Informant Magazine


On the ’Net
SAX / XML / Parsing / IMXWriter / Delphi 5

By Keith Wood

SAX Generation of XML


Microsoft’s IMXWriter Interface

A Simple API for XML (SAX) parser breaks the input XML document into tokens, and
triggers events on attached handlers for each token (see “SAX for Delphi” in the
March 2000 Delphi Informant Magazine). These event handlers can process the events
however they choose, e.g. by looking for particular elements and their contents.

One of the extra abilities included in the latest Properties specific to the IMXWriter interface let you
Microsoft XML package (MSXML v3) is support control the appearance of the resulting document.
for SAX generation in its IMXWriter interface. Figure 1 shows the declaration for the interface.
An object created through this interface also
expresses each of the other handler interfaces. The output property retrieves or redirects the XML
Invoking handler methods on such an object document generated by the writer. If assigned an
causes it to generate an XML document that object that implements IStream, the output is sent
reproduces those events. The result is available directly to that stream. If not assigned a value, or if
through the output property, which can be refer- it’s assigned an empty string, the output appears as a
enced as a WideString value, or piped directly to string value when this property is read. Assigning an
an IStream object. empty string also clears the internal buffer in prepara-
tion for generating the next section of the document.
So, instead of instantiating a SAX parser and In this way, the memory requirements of the writer
supplying your own handlers, you can generate a are reduced. The output is also reset whenever a
document by creating an IMXWriter object and startDocument event occurs.
calling its handler methods as if you’re the parser.
An alternative use is to transform an existing Set or return the encoding scheme used by the
XML document by having a SAX parser read it, writer through the encoding property. If you’re
pass the events through a SAX filter (which does retrieving the output as a string value, this setting is
the actual transformation), and direct its output ignored since all strings are UTF-16 encoded.
to a writer.
Set the byteOrderMark property to True to have
IMXWriter Interface the writer generate a byte order mark for appropri-
Objects created through the CoClass for the ate encodings. When False, no byte order mark
IMXWriter interface also implement the is included. A byte order mark is never produced
IVBSAXContentHandler, IVBSAXDTDHandler, when output is retrieved as a string.
IVBSAXLexicalHandler, and IVBSAXDeclHandler
interfaces. You invoke these to generate an XML When set to True, the indent property causes the
document. In principle, piping the events from a output document to be formatted for improved
SAX parser directly to a writer produces exactly readability. Each level of elements is indented by
the same document on output as on input. How- one tab, and opening tags appear on a new line. If
ever, certain input deemed not significant by the set to False, the XML appears without breaks.
XML specification may be skipped or generated
differently. Also, the output encoding could be The standalone property controls the appearance
different from that of the input. of the standalone declaration in the XML prolog.

26 June 2001 Delphi Informant Magazine


On the ’Net

// Interface: IMXWriter // Interface: IMXAttributes


// Flags: (4544) Dual NonExtensible // Flags: (4544) Dual NonExtensible
// OleAutomation Dispatchable // OleAutomation Dispatchable
IMXWriter = interface(IDispatch) IMXAttributes = interface(IDispatch)
['{ 4D7FF4BA-1565-4EA8-94E1-6E724A46F98D }'] ['{ F10D27CC-3EC0-415C-8ED8-77AB1C5E7262 }']
procedure Set_output(varDestination: OleVariant); procedure addAttribute(const strURI: WideString;
safecall; const strLocalName: WideString;
function Get_output: OleVariant; safecall; const strQName: WideString;
procedure Set_encoding(const strEncoding: WideString); const strType: WideString; const strValue: WideString);
safecall; safecall;
function Get_encoding: WideString; safecall; procedure addAttributeFromIndex(varAtts: OleVariant;
procedure Set_byteOrderMark( nIndex: SYSINT); safecall;
fWriteByteOrderMark: WordBool); safecall; procedure clear; safecall;
function Get_byteOrderMark: WordBool; safecall; procedure removeAttribute(nIndex: SYSINT); safecall;
procedure Set_indent(fIndentMode: WordBool); safecall;
procedure setAttribute(nIndex: SYSINT;
function Get_indent: WordBool; safecall;
const strURI: WideString;
procedure Set_standalone(fValue: WordBool); safecall;
const strLocalName: WideString;
function Get_standalone: WordBool; safecall;
const strQName: WideString;
procedure Set_omitXMLDeclaration(fValue: WordBool);
const strType: WideString; const strValue: WideString);
safecall;
safecall;
function Get_omitXMLDeclaration: WordBool; safecall;
procedure setAttributes(varAtts: OleVariant); safecall;
procedure Set_version(const strVersion: WideString);
procedure setLocalName(nIndex: SYSINT;
safecall;
const strLocalName: WideString); safecall;
function Get_version: WideString; safecall;
procedure setQName(nIndex: SYSINT;
procedure Set_disableOutputEscaping(fValue: WordBool);
safecall; const strQName: WideString); safecall;
function Get_disableOutputEscaping: WordBool; safecall; procedure setType(nIndex: SYSINT;
procedure flush; safecall; const strType: WideString); safecall;
property output: OleVariant procedure setURI(nIndex: SYSINT;
read Get_output write Set_output; const strURI: WideString); safecall;
property encoding: WideString procedure setValue(nIndex: SYSINT;
read Get_encoding write Set_encoding; const strValue: WideString); safecall;
property byteOrderMark: WordBool end;
read Get_byteOrderMark write Set_byteOrderMark;
property indent: WordBool
read Get_indent write Set_indent; Figure 2: The IMXAttributes interface.
property standalone: WordBool
read Get_standalone write Set_standalone;
property omitXMLDeclaration: WordBool
read Get_omitXMLDeclaration provides the necessary functionality (see Figure 2). An object that
write Set_omitXMLDeclaration; expresses this interface also implements IVBSAXAttributes, allowing it
property version: WideString to be passed directly to the startElement call.
read Get_version write Set_version;
property disableOutputEscaping: WordBool
read Get_disableOutputEscaping Create an object of this type through the CoSAXAttributes class’
write Set_disableOutputEscaping; Create method. Using the CoSAXAttributes30 class instead ties the
end; object to this version of the XML package — normally the latest
version is returned.
Figure 1: The IMXWriter interface.
Add an attribute to the end of the list with the addAttribute
The default is to omit it (a setting of False). You can exclude the method, which takes the name(s) of the attribute, as well as its
entire XML prolog by setting the omitXMLDeclaration property to type and string value. Send a single space if a parameter isn’t
True. By default it’s set to False to include the prolog. Set or retrieve known. No check is made for a pre-existing attribute with the
the XML version declaration from the prolog through the version same name. For performance reasons, this is left to the user to
property. It defaults to 1.0. implement if necessary. The addAttributeFromIndex method adds
an attribute to the end of the list whose value is equal to the
The disableOutputEscaping property determines whether text is specified entry from the input object.
escaped before being written out. When True, text is not escaped,
which may result in a malformed document. When False (the The clear method empties the attribute list, making it ready for reuse.
default), the text is escaped, i.e. the standard meta-characters (like <) It doesn’t free the associated memory. Delete the specified attribute by
are replaced by their corresponding entity references (like &lt;). index, starting at zero, with the removeAttribute method.

Clear the internal buffer to its output stream or string with the flush Given an attribute’s position (starting from zero), set its name(s) and
method. This happens automatically when the output property is value to the specified arguments through the setAttribute method. Use
accessed, or when the endDocument event occurs. the setAttributes method to copy the contents of another attribute
collection. It’s probably more efficient to reuse an existing object than
IMXAttributes Interface to generate a copy.
Generating an element requires that its attributes (if any) be specified.
The IVBSAXAttributes interface from SAX defines how to extract Update the local name for the specified attribute with the setLocalName
information about a set of attributes, but says nothing about how method. Similarly, alter the qualified name of the nominated attribute
to set up those details in the first place. The IMXAttributes interface through the setQName method. Use the setType method to change

27 June 2001 Delphi Informant Magazine


On the ’Net

{ Generate DTD and contents. }


procedure GenerateDTD;
var
Wide1, Wide2, Wide3: WideString;
BaseId: string;
begin
Wide1 := MovieWatcherTag;
Wide2 := XMLDTDFile;
LexicalHandler.StartDTD(Wide1, Empty, Wide2);
Wide1 := GIFType;
Wide2 := GIFPubId;
Wide3 := GIFSysId;
DTDHandler.NotationDecl(Wide1, Wide2, Wide3);
Wide1 := HTMLType;
Wide2 := HTMLPubId;
Wide3 := HTMLSysId;
DTDHandler.NotationDecl(Wide1, Wide2, Wide3);
with qryMovie do begin
First;
while not EOF do begin
BaseId := FieldByName(MovieIdField).DisplayText;
if FieldByName(LogoURLField).AsString <> '' then
begin
Wide1 := BaseId + 'Logo';
Wide2 := FieldByName(LogoURLField).DisplayText;
Wide3 := GIFType;
DTDHandler.UnparsedEntityDecl(
Wide1, Empty, Wide2, Wide3); Figure 4: SAX document generation.
end;
if FieldByName(URLField).AsString <> '' then
begin lets you retrieve that information. Use this to accumulate attri-
Wide1 := BaseId + 'URL';
butes for an element that can then be passed into the correspond-
Wide2 := FieldByName(URLField).DisplayText;
Wide3 := HTMLType; ing startElement call of the content handler.

Generation continues with calls to the appropriate handler methods,


Figure 3: Generating the DTD.
beginning with the content handler’s startDocument call, and finish-
ing with its endDocument. Between these appear other calls to include
the type for the specified attribute. Modify the nominated attribute’s the DTD, a top-level comment, a style-sheet reference, and the actual
namespace URI with the setURI method. And, finally, overwrite the content of the document. The latter occurs within the bounds of a
specified attribute’s value via the setValue method. startElement and endElement call for the main document element.

Creating a Writer To demonstrate the chunking ability of the writer, the UpdateOutput
To demonstrate how the writer is used, you need to develop a method is called after each major section of the document is com-
generator that takes data from a database and formats it into pleted. This routine adds the generated XML to the memo onscreen
XML. The data in this example are the movie-watcher details via the output property, and then resets that property ready for the next
from my previous XML articles, which are stored in six tables in a section. Setting the indent property to True before calling any handlers
Paradox database. (It’s all available for download; see end of article causes the document to be formatted for improved readability.
for details.) Before trying to access the data, be sure to set up
a Paradox BDE alias named movie-watcher, and set the path to Defining the DTD
that of the example program and tables. Since the writer is based on the SAX2 specification, it implements
the two standard extensions: IVBSAXLexicalHandler and
Obtaining a SAX writer object is easy with the CoMXXMLWriter IVBSAXDeclHandler. The first of these lets you add the DTD decla-
CoClass. Once MSXML version 3 is installed (go to http:// ration itself through its startDTD method, specifying the document
msdn.microsoft.com/xml/default.asp if you don’t have it), the type and its public and system identifiers (see Figure 3). Following
MSXML2_tlb unit is included in your uses clause, and you call this call, and before the endDTD call, you use the DTD handler
on this class to generate a new writer via its Create method. Use interface to add any notations and unparsed entities.
CoMXXMLWriter30 instead to always produce an object from
this version of the XML package, rather than the latest version You could define the entire DTD internally, including element
returned by the previous CoClass. Although the IMXWriter inter- and attribute declarations, by invoking methods on the
face derives directly from IDispatch and makes no mention of the IVBSAXDeclHandler interface. Such calls must appear between
handler interfaces, the resulting object does implement them. The the startDTD and endDTD calls.
easiest way to refer to them from Delphi is to declare variables for
each desired interface, and use the as operator to cast the writer Adding Content
accordingly. See Listing One (on page 30) for the creation code. The main information in the generated document comes from calls
to the content handler’s methods, especially startElement/endElement
Also necessary is an instance of the IMXAttributes interface, which and Characters. Data for movies in the sample document appears
provides for the writing of attribute values into a storage area. within the confines of the main element. A movies element is started
Such objects also implement the IVBSAXAttributes interface that before stepping through each database record and producing its

28 June 2001 Delphi Informant Magazine


On the ’Net
output. Upon completion, the corresponding closing tag is written.
See Listing Two (on page 30) for the generation code.

Attributes for an element are sent as part of the opening element


call. Therefore, they must be accumulated and prepared before
that time. The helper routine, AddAttribute, places the specified
attribute into the IMXAttributes object created during initializa-
tion of the document. The StartElement call then uses this to
supply the information to the writer, and subsequently clears the
list, ready for the next element.

Another helper routine, AddSimpleElement, adds a child element with


a single text node body. The element name derives from the database
field name, and the field’s value provides the content. A flag indicates
whether the text should be treated as a CDATA section, as is the
case for the synopsis data. The text is always sent via the Characters
method, but in the case of a CDATA section, this call appears
between the lexical handler’s startCDATA and endCDATA calls.

All of this comes together in the demonstration project (again; see


end of article for details). It expects the “movie-watcher” BDE alias
to point to the accompanying database tables. You can invoke the
generation process with the Generate button; the output appears
onscreen, as shown in Figure 4. To view it within a browser, use the
Save button and open the resulting file.

Conclusion
Similar in structure to generating a document through one of the
DOM offerings, the SAX writer approach offers the usual SAX
advantage of reducing memory requirements. Only a single element
or text node appears at any one time. Furthermore, this technique
lets you create the entire document, including any internal DTD —
something that most DOMs don’t allow.

Of course, you’re generating the output into memory as text.


This resource can be reduced through multiple calls to the output
property of the writer. Each time a section of the document is
returned, it’s cleared ready for the next section. Thus, you can
chunk the document and write out smaller pieces at a time.
Alternately, you can set the output property to an IStream, which
then accepts each part as it’s created.

The SAX writer is another option to consider when creating XML


documents on-the-fly. It can be used to your advantage when the
resulting document is too large to comfortably fit in memory at one
time, and when there’s no need to randomly access the nodes within
a document. ∆

This article is adapted from material for the forthcoming book Delphi
Developer’s Guide to XML [Wordware, 2001].

The database and demonstration project referenced in this article are


available on the Delphi Informant Magazine Complete Works CD
located in INFORM\2001\JUN\DI200106KW.

Keith Wood is an Australian working his way around the United States. He’s
a systems engineer with Active Health Management, based near Boston, and a
freelance technical writer. Keith started using Borland’s products with Turbo Pascal
on a CP/M machine. His forthcoming book, Delphi Developer’s Guide to XML
[Wordware, 2001], covers many other aspects of XML. You can reach him via
e-mail at kbwood@compuserve.com.

29 June 2001 Delphi Informant Magazine


On the ’Net

{ End an element tag. }


Begin Listing One — Creating a SAX writer procedure EndElement(Name: WideString);
{ Generate the XML document as text. } begin
procedure TfrmWriterXML.btnGenerateClick(Sender: TObject); ContentHandler.EndElement(NoValue, NoValue, Name);
const end;
Empty: WideString = '';
NoValue: WideString = ' '; { Save an attribute for adding to an element. }
var procedure AddAttribute(Name, Value: WideString);
XMLDoc: IMXWriter; begin
ContentHandler: IVBSAXContentHandler; Attributes.AddAttribute(NoValue, NoValue, Name,
DTDHandler: IVBSAXDTDHandler; NoValue, Value);
LexicalHandler: IVBSAXLexicalHandler; end;
Attributes: IMXAttributes;
{ Add the document generated so far to the output. } { Add a simple element that only contains text. }
procedure UpdateOutput; procedure AddSimpleElement(Field: TField;
begin AsCDATA: Boolean = False);
memXML.Lines.Text := memXML.Lines.Text + XMLDoc.Output; var
XMLDoc.Output := Empty; Value: WideString;
end; begin
StartElement(ModifyName(Field.FieldName));
{ Generate XML prolog, style sheet reference, if AsCDATA then
and main element. } LexicalHandler.StartCDATA;
procedure GenerateDocument; Value := Field.DisplayText;
var if Value = '' then
Wide1, Wide2: WideString; Value := NoValue;
begin ContentHandler.Characters(Value);
ContentHandler.StartDocument; if AsCDATA then
GenerateDTD; LexicalHandler.EndCDATA;
Wide1 := XMLComment; EndElement(ModifyName(Field.FieldName));
LexicalHandler.Comment(Wide1); end;
Wide1 := XMLStyleTag;
Wide2 := XMLStyleAttrs; { Compile elements for the stars of the movie. }
ContentHandler.ProcessingInstruction(Wide1, Wide2); procedure GenerateStars;
UpdateOutput; begin
StartElement(MovieWatcherTag); with qryStars do begin
GenerateMovies; StartElement(StarringTag);
UpdateOutput; First;
GenerateCinemas; while not EOF do begin
UpdateOutput; AddSimpleElement(FieldByName(StarField));
GenerateScreenings; Next;
EndElement(MovieWatcherTag); end;
ContentHandler.EndDocument; EndElement(StarringTag);
UpdateOutput; end;
end; end;

begin { Generate elements for each movie. }


Screen.Cursor := crHourglass; procedure GenerateMovies;
memXML.Lines.Clear; var
{ Instantiate the XML writer. } BaseId: string;
XMLDoc := CoMXXMLWriter.Create; begin
try StartElement(MoviesTag);
ContentHandler := XMLDoc as IVBSAXContentHandler; with qryMovie do begin
DTDHandler := XMLDoc as IVBSAXDTDHandler; First;
LexicalHandler := XMLDoc as IVBSAXLexicalHandler; while not EOF do begin
Attributes := CoSAXAttributes.Create; BaseId := FieldByName(MovieIdField).DisplayText;
XMLDoc.Indent := True; AddAttribute(Id, BaseId);
{ Generate the structure. } AddAttribute(Rating,
GenerateDocument; FieldByName(RatingField).DisplayText);
finally if FieldByName(LogoURLField).AsString <> '' then
{ Release the XML writer. } AddAttribute(ModifyName(FieldByName(
Attributes := nil; LogoURLField).FieldName), BaseId + 'Logo');
XMLDoc := nil; if FieldByName(URLField).AsString <> '' then
Screen.Cursor := crDefault; AddAttribute(ModifyName(FieldByName(
end; URLField).FieldName), BaseId + 'URL');
end; StartElement(MovieTag);

End Listing One AddSimpleElement(FieldByName(NameField));


AddSimpleElement(FieldByName(LengthField));
AddSimpleElement(FieldByName(DirectorField));
GenerateStars;
Begin Listing Two — Adding movie content AddSimpleElement(FieldByName(SynopsisField), True);
{ Start a new element tag. } EndElement(MovieTag);
procedure StartElement(Name: WideString); Next;
begin end;
ContentHandler.StartElement(NoValue, NoValue, Name, end;
Attributes as IVBSAXAttributes); EndElement(MoviesTag);
Attributes.Clear; end;
end;
End Listing Two

30 June 2001 Delphi Informant Magazine


Distributed Delphi
Internet / IP Helper API / TCP/IP / Delphi 5

By John C. Penman

IP Helper API
Part I: Retrieving Local Configuration Parameters and
Network Statistics

W ith the explosive growth of the Internet in recent years, networking has become
a fact of life on modern Windows platforms, as it has been on UNIX systems
long before the arrival of Windows. Like any system of software and hardware
working together, it needs maintenance to perform satisfactorily over time.

Thus, tools to manage TCP/IP networking were ery of data between peers. The User Datagram
developed for system administrators, power users, Protocol (UDP) is a protocol for transmission
and the plain curious. Of course, the tools of packets of data between peers with no mecha-
described in this article only represent a small part nisms for error recovery.
of the spectrum of network management tools.
Microsoft provides tools, which have their peers These tools use a set of functions known as the IP
on UNIX, including Linux systems, that run from Helper API. These functions are implemented in
the command line (see Figure 1). These tools work the IPHlpAPI.dll. This DLL should be present in
intimately with protocols such as IP, ICMP, TCP, the \System32 directory on Windows NT4/2000
and UDP. and in the \System directory on Windows 98/Me.
Note, however, that the API isn’t available for Win-
The Internet Protocol (IP), which is defined by dows 95.
RFC791, is the glue that makes the Internet pos-
sible. IP is responsible, among its many tasks, for In this two-part series of articles about the IP
the delivery of datagrams between the sender and Helper API, I’ll demonstrate how to use these
receiver. The Internet Control Message Protocol functions. Part I describes how to retrieve local
(ICMP), which sits on top of IP, is the protocol configuration parameters and network statistics
that manages flow control and error reporting for a machine. Part II will explore routing issues
for TCP/IP. The Transmission Control Protocol (Route.exe) and address resolution (ARP.exe).
(TCP) is a stream protocol that guarantees deliv-
As part of the JEDI (Joint Endeavor of Delphi
Innovators) effort, I used Dr. Bob’s HeadConv util-
Tool Platform(s) Description
ity to translate the C header files to their Delphi
IPConfig.exe Windows 98, NT4, Configures IP addresses and retrieves equivalents (see Figure 2). The main interface file is
and 2000 network adapter information. IPHlpAPI.pas; the other units define the constants
WinIPCfg.exe Windows 95 Configures IP addresses and retrieves and data structures.
network adapter information.
NetStat.exe Windows 98, NT4, Retrieves statistics for the TCP, UDP, IP, Marcel van Brakel, a leading figure in the JEDI
and 2000 and ICMP protocols. effort, fine-tuned the Delphi units, and made them
Route.exe Windows 98, NT4, Displays and configures the routing table. conform to Borland’s coding standards. These inter-
and 2000
face units should be available from the JEDI Web
ARP.exe Windows 98, NT4, Displays and configures the ARP table.
and 2000
site (www.delphi-jedi.org), and are also available
from the Delphi Informant Magazine Web site as
Figure 1: Command-line tools for TCP/IP network management. part of the demonstration projects for this article

31 June 2001 Delphi Informant Magazine


Distributed Delphi
C Header File Interface Unit Description Function Operating System

IPIfCons.h IPIfCons.pas Constants AddIPAddress Windows 2000


CreateIpForwardEntry Windows 98, NT4.0 (SP4), and 2000
IPExport.h IPExport.pas Definitions for IP and ARP
CreateIpNetEntry Windows 98, NT4.0 (SP4), and 2000
IPHlpAPI.h IPHlpAPI.pas Interface unit DeleteIPAddress Windows 2000
IPRtrMIB.h IPRtrMIB.pas Definitions for routing DeleteIpForwardEntry Windows 98, NT4.0 (SP4), and 2000
IPTypes.h IPTypes.pas Definitions for configuration DeleteIpNetEntry Windows 98, NT4.0 (SP4), and 2000
FlushIpNetTable Windows 2000
Figure 2: Delphi units. GetAdaptersInfo Windows 98 and 2000
GetBestInterface Windows 98 and 2000
1) IP Configuration and Adapter Information GetIcmpStatistics Windows 98, NT4.0 (SP4), and 2000
§ GetNetworkParams obtains IP configuration parameters. GetIfEntry Windows 98, NT4.0 (SP4), and 2000
§ GetAdaptersInfo obtains information on adapters on a PC. GetInterfaceInfo Windows 98 and 2000
§ IpReleaseAddress releases an assigned IP address via the DHCP protocol. GetIpAddrTable Windows 98, NT4.0 (SP4), and 2000
§ IpRenewAddress renews an assigned IP address via the DHCP protocol. GetIpForwardTable Windows 98, NT4.0 (SP4), and 2000
§ GetInterfaceInfo obtains interface information such as the MAC address. GetIpNetTable Windows 98, NT4.0 (SP4), and 2000
§ DeleteIPAddress deletes an IP address. GetIpStatistics Windows 98, NT4.0 (SP4), and 2000
§ AddIPAddress adds an IP address. GetNetworkParams Windows 98 and 2000
2) Network Statistics GetTcpStatistics Windows 98, NT4.0 (SP4), and 2000
GetTcpTable Windows 98, NT4.0 (SP4), and 2000
§ GetTcpTable obtains a table for the TCP protocol. GetUdpStatistics Windows 98, NT4.0 (SP4), and 2000
§ GetUdpTable obtains a table for the UDP protocol.
§ GetIpStatistics obtains statistics for the IP protocol. GetUdpTable Windows 98, NT4.0 (SP4), and 2000
§ GetIcmpStatistics obtains statistics for the ICMP protocol. IpReleaseAddress Windows 98 and 2000
§ GetTcpStatistics obtains statistics for the TCP protocol. IpRenewAddress Windows 98 and 2000
§ GetUdpStatistics obtains statistics for the UDP protocol. SetIpForwardEntry Windows 98, NT4.0 (SP4), and 2000
SetIpNetEntry Windows 98, NT4.0 (SP4), and 2000
3) Route Configuration
Figure 4: IP Helper functions used by the four projects described
§ GetIpForwardTable obtains a table of routes. in this series.
§ SetIpForwardEntry adds a route.
§ DeleteIpForwardEntry deletes a route. Value Meaning

4) ARP Configuration ERROR_BUFFER_OVERFLOW The buffer size parameter,


§ GetIpNetTable obtains a table of ARP cache entries. pOutBufLen, is too small to hold
the adapter information. The
§ SetIpNetEntry modifies an existing ARP entry in the cache. pOutBufLen parameter points to
§ DeleteIpNetEntry deletes an ARP entry. the required size.
§ FlushIpNetTable flushes the ARP cache. ERROR_INVALID_PARAMETER The pOutBufLen parameter is nil,
the calling process doesn’t have
5) IP Packet Filtering
read/write access to the memory
pointed to by pOutBufLen, or the
Figure 3: Road map for the IPHlpAPI functions. calling process doesn’t have write
access to the memory pointed to
(see end of article for details). As an aside, Dr. Bob has just completed by the pAdapterInfo parameter.
a major update to HeadConv, which will become an important tool ERROR_NO_DATA No adapter information exists for
for use by Project JEDI’s DARTH Team. Should you contemplate the local computer.
translating Microsoft C header files to Delphi, you could save yourself ERROR_NOT_SUPPORTED The function just called isn’t
many hours of sweat by using Dr. Bob’s HeadConv utility. Or better supported by the operating system
on the local computer.
still, contribute toward the JEDI project to make the Windows and
Other If the function fails, use
Linux APIs accessible to Delphi developers.
GetLastError to get the message
string for the returned error.
To explore the territory covered in this series of articles, follow the
road map in Figure 3. The functions are categorized into five groups Figure 5: Error codes and their meanings.
by functionality. In terms of functionality, Windows 98 is a poor
relation as the majority of the IP Helper functions are not available. The first parameter, pFixedInfo, is a pointer to the FIXED_INFO
Please note that every function in the API won’t be discussed here data structure, which the function fills on return. The second
because there are too many to cover. For this series, I constructed parameter, pOutBufLen, defines the length of the structure. Like
four projects that use the functions shown in Figure 4. some of Microsoft’s functions, it’s sometimes necessary to call the
function twice. The first call is to get the size required to
IP Configuration and Adapter Information allocate memory for the data structure, while the second populates
To retrieve network parameters for the local computer, call the the data structure. The strategy is to set the appropriate data
GetNetworkParams function, which is defined in IPHlpAPI.pas. Its structure to nil, pFixedInfo in this case, and the length of this
prototype is as follows: structure to zero in the pOutBufLen parameter. On return, if
the function returns an error of ERROR_BUFFER_OVERFLOW
function GetNetworkParams(pFixedInfo: PFIXED_INFO; or ERROR_INSUFFICIENT_BUFFER, call the function again
var pOutBufLen: ULONG): DWORD; stdcall;

32 June 2001 Delphi Informant Magazine


Distributed Delphi
Value Meaning
BufferSize := 0;
Res := GetNetworkParams(nil, BufferSize); BROADCAST_NODETYPE Uses IP broadcasting.
if (Res = ERROR_BUFFER_OVERFLOW) or PEER_TO_PEER_NODETYPE Uses a NETBIOS server such
(Res = ERROR_INSUFFICIENT_BUFFER) then begin as WINS.
NetworkParameters := AllocMem(BufferSize); MIXED_NODETYPE Uses IP broadcasting, and if this
try fails, uses WINS.
if GetNetworkParams(NetworkParameters,
HYBRID_NODETYPE Uses WINS, and if this fails, uses
BufferSize) = ERROR_SUCCESS then begin
IP broadcasting.
{ Rest of code. }
Figure 8: NodeType values.

Figure 6: The code to retrieve data for GetNetworkParams.


PIpAdapterInfo = ^TIpAdapterInfo;
FIXED_INFO = record _IP_ADAPTER_INFO = record
HostName: array [0..MAX_HOSTNAME_LEN + 3] of Char; Next: PIpAdapterInfo;
DomainName: array[0..MAX_DOMAIN_NAME_LEN + 3] of Char; ComboIndex: DWORD;
CurrentDnsServer: PIP_ADDR_STRING; AdapterName:
DnsServerList: IP_ADDR_STRING; array [0..MAX_ADAPTER_NAME_LENGTH + 3] of Char;
NodeType: UINT; Description:
ScopeId: array [0..MAX_SCOPE_ID_LEN + 3] of Char; array [0..MAX_ADAPTER_DESCRIPTION_LENGTH + 3] of Char;
EnableRouting: UINT; AddressLength: UINT;
EnableProxy: UINT; Address:
EnableDns: UINT; array [0..MAX_ADAPTER_ADDRESS_LENGTH - 1] of BYTE;
end; Index: DWORD;
PFIXED_INFO = ^FIXED_INFO; Type_: UINT;
TFixedInfo = FIXED_INFO; DhcpEnabled: UINT;
CurrentIpAddress: PIP_ADDR_STRING;
IpAddressList: IP_ADDR_STRING;
GatewayList: IP_ADDR_STRING;
Figure 7: The data structure for FIXED_INFO.
DhcpServer: IP_ADDR_STRING;
HaveWins: BOOL;
PrimaryWinsServer: IP_ADDR_STRING;
with the new value assigned to the second parameter. On the SecondaryWinsServer: IP_ADDR_STRING;
LeaseObtained: time_t;
other hand, if the function returns a different error (see Figure LeaseExpires: time_t;
5), then your application should handle the error and abort grace- end;
fully. This is the approach used in the project. The code snippet in
Figure 6 shows this strategy clearly in the case of the GetNetwork-
Params function. Figure 9: The data structure of IP_ADAPTER_INFO.

The prototype of the FIXED_INFO data


structure, which is defined in IPTypes.pas, is
shown in Figure 7.

The HostName field is the name of your


computer on the LAN as recognized by
the Domain Name Service (DNS). The
DomainName field specifies the domain of
which your computer is a member. The
CurrentDnsServer field is the IP address of
your DNS server. The DnsServerList, which is
a linked list structure, contains the list of DNS
servers your machine uses. This field is of type
IP_ADDR_STRING, a linked list structure.
The NodeType specifies how the system should
Figure 10: Output after a call to GetAdaptersInfo.
resolve NETBIOS names.
mine the configuration details of all adapters on the local machine.
There are several possible ways to resolve NETBIOS names, as A network interface is an adapter that’s either a dialup connection
shown in Figure 8. The ScopeId field specifies a string to append like a modem or a hardware device like a network card. For example,
to the NETBIOS name to group computers. The EnableRouting a machine could have two network cards and a dialup connection;
field specifies whether the system will route IP packets between this machine has three adapters. Calling the GetAdaptersInfo function
networks to which it’s connected. The EnableProxy field specifies provides a wealth of detail on the adapter(s) detected on the local
whether the system will act as a WINS proxy agent on a network. machine. The prototype of this function is defined in IPHlpAPI.pas:
The last field, EnableDns, specifies whether NETBIOS will query
DNS for names that cannot be resolved by WINS broadcast, or function GetAdaptersInfo(pAdapterInfo: PIP_ADAPTER_INFO;
the LMHOSTS file. var pOutBufLen: ULONG): Longint; stdcall;

Retrieving Adapter Information The function returns information on the adapters on the local
In addition to the IP configuration parameters, it’s also useful to deter- machine in the first parameter, pAdapterInfo, which is a pointer to

33 June 2001 Delphi Informant Magazine


Distributed Delphi
Project Purpose Equivalent
try
Name Microsoft Utility
Res := GetInterfaceInfo(InterfaceInfo, @BufferSize);
if Res = ERROR_SUCCESS then begin dIPConfig IP Configuration and Information IPConfig.exe
Found := FALSE; dNetStat Statistics NetStat.exe
dwIndex := StrToInt(Index); dRoute Route Configuration Route.exe
for Count := 0 to InterfaceInfo^.NumAdapters - 1 do dARP Address Resolution ARP.exe
begin
if InterFaceInfo^.Adapter[ Figure 12: Developed projects.
Count].Index = dwIndex then
begin in the DhcpServer field. When DHCP is enabled, the LeasedObtained
Found := True; and LeaseExpires fields will have valid values. The LeasedObtained field
Res := IpReleaseAddress( is the date and time of when the IP address was set. The LeaseExpires
InterFaceInfo^.Adapter[Count]);
field specifies the time when the IP address will “die” and return to the
if Res <> NO_ERROR then
ShowMessage('Failed to release IP ' +
pool of IP addresses.
'addresses for this adapter. Error = ' +
IntToStr(Res)) What’s DHCP? DHCP, which is based on the BOOTP protocol, is a
else protocol for dynamically assigning IP addresses to hosts on a network.
ShowMessage('IP address has been released' + (The BOOTP protocol itself provides the capability for a client, which
' for this adapter...');
is usually diskless, to discover its own IP address, the address of the
Break;
end;
BOOTP server, and the name of a configuration file to be loaded into
end; memory and executed.) DHCP provides the capability of automatic
if not Found then allocation of reusable network addresses and additional configuration
ShowMessage('Adapter not found. '); options. The DHCP protocol is defined by the standard documents,
end; RFC1534, RFC2131, and RFC2132.
finally
FreeMem(InterFaceInfo);
DHCP consists of two components: a protocol for delivering config-
end;
uration parameters from a DHCP server to a client, and a mechanism
for allocation of network addresses to clients. Like many Internet
Figure 11: Releasing the IP address. protocols, DHCP is a client-server protocol in which a designated

the IP_ADAPTER_INFO record. Figure 9


shows the structure in detail. The returned
pAdapterInfo structure is the first in a linked
list through the Next field. Next is nil for the
last adapter.

The IP_ADAPTER_INFO is a massive


data structure, containing many interesting
fields. As with GetNetworkParams, make
two calls: the first to get the required size of
the buffer; the second to populate the data
structure. Figure 10 shows the output from
a call to GetAdaptersInfo.

Because there are too many fields to examine Figure 13: Help information on how to use dIPConfig.
in this article, just a few important fields
will be examined. The function enumerates
the name (AdapterName), the physical
address (Address), and a short description
(Description) for each adapter on a machine.
In the past, it was a fairly difficult task to
obtain the address of a network card by
other means, but by using this call you can
obtain the address fairly easily. Before leaving
GetAdaptersInfo, take a look at the fields
relating to the Dynamic Host Configuration
Protocol (DHCP), which are DhcpEnabled,
DhcpServer, LeasedObtained, and LeaseExpires.

If DHCP is disabled on your system, the


DhcpEnabled field is shown as disabled (refer
to Figure 10). If DHCP is enabled, then you Figure 14: Typical output after a call to dIPConfig for general network parameters for
should see the IP address of the DHCP server the local machine.

34 June 2001 Delphi Informant Magazine


Distributed Delphi
DHCP server allocates network addresses, and delivers configuration Instead, this type of array, which is common in Windows APIs, tells
parameters to clients that request them. us that this is a variable length array. To index arrays of this type
without generating a run-time error (range check error), you should
One of the benefits of using this protocol in a large network that has switch off range checking in the compiler settings.
many machines (100 or more) is the reduction in cost of administering
the allocation of IP addresses. More importantly, in these days of IP The IP_ADAPTER_INDEX_MAP record returns a list of adapter
address shortages, DHCP permits the reuse of IP addresses from a pool names you search to find a matching adapter. When the appropri-
of IP addresses for the local area network. ate adapter is found, use the index (in the array) of that adapter to
select the adapter to which to release the IP address, as shown in
Renewing and Releasing IP Addresses Figure 11. To renew an IP address, use the same approach as with
If a LAN has a DHCP server, and a machine has an IP address the IpRenewAddress function.
that has been assigned by DHCP, you can renew and release the IP
address for a machine. Why do you need the capability to renew and Changing IP Addresses
release an IP address? As I just mentioned, there’s a shortage of IP In Windows 2000, it’s easy to add or delete IP addresses on your
addresses, and there are two functions available to resolve this short- machine, but there’s one little catch. It’s not possible to modify an
age: IpRenewAddress and IpReleaseAddress. Their prototypes, which are existing IP address. To add a new IP address, use the AddIPAddress
defined in IPHlpAPI.pas, are as follows: function. To delete an existing IP address, use the DeleteIPAddress
function. Their prototypes are as follows:
function IpRenewAddress(
var AdapterInfo: IP_ADAPTER_INDEX_MAP): DWORD; stdcall; function AddIPAddress(Address: IPAddr; IpMask: IPMask;
IfIndex: DWORD; var NTEContext, NTEInstance: ULONG):
DWORD; stdcall;
function IpReleaseAddress(
var AdapterInfo: IP_ADAPTER_INDEX_MAP): DWORD; stdcall;
function DeleteIPAddress(NTEContext: ULONG):
DWORD; stdcall;
DHCP leases an IP address to a client on the network for a fixed
time, and the lease has to be renewed frequently. When the lease As with the IpRenewAddress and IpReleaseAddress functions, you
expires, the client uses the IpRenewAddress function to renew the need to know the index of the adapter to which you want to add,
lease on the IP address. When the client no longer requires the or from which to delete, an IP address. In addition, every IP address
leased IP address, the client calls the IpReleaseAddress function to has a unique context number that’s required for the DeleteIPAddress
release the IP address. The released IP address returns to the pool function to work properly. Call GetAdaptersInfo to obtain the index
of IP addresses for reuse by DHCP for new clients. for the adapter, and the context number for the IP address.

A call to IpRenewAddress or IpReleaseAddress targets a specific adapter, To add a new IP address, assign the Address parameter with the desired IP
and a specific IP address is assigned to a specific adapter. To select the address, the IpMask to the network mask, and the required index, IfIndex,
adapter, call the GetInterfaceInfo function to obtain the index to the obtained from GetAdaptersInfo. To delete an existing IP address, get the
adapter. In Windows, every adapter or network interface card (NIC) context number for that IP address. After calling GetAdaptersInfo, scan
has a unique index. The prototype for this function, which is also the IpAddressList field, which is a linked-list structure, to match the IP
defined in IPHlpAPI.pas, is as follows: address to delete. If there’s a match, get the context number from the
Context field, and pass it to the DeleteIPAddress function.
function GetInterfaceInfo(pIfTable: PIP_INTERFACE_INFO;
dwOutBufLen: PULONG): DWORD; stdcall; Network Statistics
The functions that retrieve statistics for the IP, ICMP, TCP, and UDP
The first parameter in the following code is a pointer to the protocols are straightforward, so they will not be described here. How-
IP_INTERFACE_INFO data structure. The second parameter speci- ever, there’s a project called dNetStat that encapsulates some of the
fies the size of the buffer required to return all information: functionality of the real world NetStat utility (available for download;
see end of article for details). For more information on how to retrieve
PIP_INTERFACE_INFO = ^_IP_INTERFACE_INFO; network statistics, consult the latest MSDN (Microsoft Developer
IP_INTERFACE_INFO = record Network) documentation.
NumAdapters : Longint;
Adapter : array [0..1-1] of IP_ADAPTER_INDEX_MAP;
end; The Projects
Take a look at the four projects developed for this series (see Figure
12). dIPConfig and dNetStat are discussed in detail here; routing and
The NumAdapters field is the number of adapters present on addressing issues will be covered in the next article. As you’ll discern from
the local machine. The second field, Adapter, is an array of Figure 12, the dIPConfig and dNetStat projects emulate the functionality
IP_ADAPTER_INDEX_MAP data structures: of IPConfig and NetStat programs provided by Microsoft. Like these
utilities, the Delphi projects are compiled and used as consoles. To obtain
PIpAdapterIndexMap = ^TIpAdapterIndexMap; help on how to use these command-line projects, simply call the utility:
_IP_ADAPTER_INDEX_MAP = record
Index: ULONG;
C:\dipconfig /?
Name: array [0..MAX_ADAPTER_NAME - 1] of WCHAR;
end;
This will generate a screen of help information, as shown in Figure 13.
As another example on how to use dIPConfig, the following com-
The array appears to have zero elements allocated, but that’s not so. mand adds a new IP address to the adapter:

35 June 2001 Delphi Informant Magazine


Distributed Delphi
dIPConfig -a 192.168.1.200 255.255.255.0 16777219

The first parameter tells the utility that you want to add a new IP
address, 192.168.1.200, to the adapter whose interface is 16777219.
You must specify the subnet mask, which in this example is
255.255.255.0. Remember that to get the interface for the adapter to
add an IP address, you must call dIPConfig like this:

dIPConfig -pa

From the listing produced by dIPConfig, look up the index value to


get the interface for that adapter.

To delete the IP address, you must specify the context that’s associated
with the IP address you wish to remove. For example, to remove the IP
address just added in the previous example, call dIPConfig like this:

dIPConfig -d 192.168.1.200

To display network information for the local machine, call dIPConfig


like this:

dIPConfig -pn

The result should look like Figure 14.

Conclusion
You’ve now learned the rudiments of the IP Helper API, and found
out how to retrieve IP configuration details for any adapter on the
local machine. If you like, you can change some of the settings in the
configuration of adapters. In Part II, we’ll explore the other functions
in the IP Helper API to perform routing and address resolution. ∆

Additional Resources
UNIX Network Programming: Networking APIs: Sockets and XTI,
Vol. 1 by W. Richard Stevens [Prentice Hall, 1997].
Internetworking with TCP/IP: Design, Implementation, and Internals,
Vol. 2 by Douglas E. Comer and David Stevens [Prentice Hall, 1994].
Network Programming for Microsoft Windows by Anthony Jones
and Jim Ohlund [Microsoft Press, 1999].

The dIPConfig and dNetStat projects referenced in this article are avail-
able on the Delphi Informant Magazine Complete Works CD located in
INFORM\2001\JUN\DI200106JP.

John Penman is the owner of Craiglockhart Software, which specializes in Internet


and intranet software solutions. John can be reached at jcp@craiglockhart.com.

36 June 2001 Delphi Informant Magazine


At Your Fingertips
Qt / Cross-platform Development / Linux / Kylix

By Bruno Sonnino

Kylix Tips Redux


Rotated Text, Detecting Drives, etc.

T his month, I’d like to revisit a couple of Delphi tips from previous columns,
and demonstrate how the same tasks can be accomplished with Kylix. The first
concerns a tip published in the December 2000 issue of Delphi Informant Magazine.

Rotating Text rotated at any angle. You can even draw text
The article “Fancy Menus, etc.” showed how mirrored horizontally and vertically.
to create a new font using the CreateFont and
CreateFontIndirect functions. These are Windows To draw rotated text, you must use the
API functions, so they aren’t available in Kylix. QPainter_rotate function. It’s declared like this:

In Delphi, all drawing is done using the TCanvas procedure QPainter_rotate(handle: QPainterH;
class. This hasn’t changed in Kylix, but many a: Double); cdecl;

internal changes have been made to TCanvas. In


Windows, TClass encapsulates a Windows device This function rotates the coordinate system by the
context (DC), and its Handle property is a handle specified angle a, given in degrees. The first param-
to this device context. eter is the handle of the QPainter object, obtained
using TCanvas.Handle. You don’t specify an angle
Obviously, things must be done differently with at which to draw the text with this function;
CLX. The Qt API declares a class named QPainter instead, you rotate the whole coordinate system.
that is responsible for drawing on a QPaintDevice. This is similar to rotating a sheet of paper while
The QPainter class is similar to a TCanvas, and has continuing to draw horizontally.
methods to draw figures such as lines, circles, text,
and other simple shapes. In CLX, the TCanvas The idea is to rotate the coordinate system, draw
class encapsulates QPainter, and its Handle prop- the text, and then rotate the coordinate system
erty is a handle (QPainterH) to a QPainter object. again to its original position. Note that the rota-
tion affects all drawing, not just text. For example,
Once you have a valid canvas, you have the a line from 0, 0 to 100, 0, which would be
internal QPainter object and can access it with horizontal on a normal coordinate system, would
TCanvas.Handle. This wouldn’t matter much if be rotated if you had issued a QPainter_rotate
the TCanvas class encapsulated everything avail- statement. It’s also important to remember that all
able in QPainter, but it doesn’t. Therefore, there QPainter coordinate transformations are relative.
are many things you can do with QPainter that Thus, if you issue this statement:
you cannot do with TCanvas. These include:
2D transformations, e.g. rotation and translation QPainter_rotate(Canvas.Handle, 10)
viewing transformations, e.g. viewports and
windows twice, the coordinate system will be rotated by 20
drawing complex shapes, e.g. Bezier curves, degrees.
chords, and multiple line segments
Another important consideration concerns the
One of the things you cannot do with the rotation of reference points. When you rotate the
TCanvas provided by VisualCLX is draw text coordinate system, the reference point (in rela-
at various angles; you can only draw horizontal tion to the upper-left corner) used by the TextOut
text using the TCanvas.TextOut method. With method won’t be the same; it will be rotated at
QPainter methods, however, you can draw text the same angle.

37 June 2001 Delphi Informant Magazine


At Your Fingertips

procedure TForm1.FormPaint(Sender: TObject); procedure TForm1.FormCreate(Sender: TObject);


var var
i : Integer; MntDrives : PIOFile;
DevPoint, PrPoint : TPoint; MountEntry : PMountEntry;
begin begin
// We will draw in the middle of the form. // Open /etc/mtab.
MntDrives := setmntent('/etc/mtab','r');
DevPoint.X := ClientWidth div 2;
if MntDrives <> nil then
DevPoint.Y := ClientHeight div 2;
// Process drives until there are no more.
// Clear the form before drawing.
repeat
Canvas.FillRect(ClientRect);
// Get next entry.
// Draw text 18 times. MountEntry := getmntent(MntDrives);
for i := 0 to 17 do begin // Found?
// Rotate canvas 20 degrees. if MountEntry <> nil then
QPainter_rotate(Canvas.Handle,20); with MountEntry^ do
// Convert device coordinates to model coordinates. // Add a line for each drive entry found.
QPainter_xFormDev(Canvas.Handle, PPoint(@PrPoint), with ListView1.Items.Add do begin
PPoint(@DevPoint)); Caption := string(mnt_fsname);
// Draw the text. SubItems.Add(string(mnt_type));
Canvas.TextOut(PrPoint.X + 50, PrPoint.Y, SubItems.Add(string(mnt_dir));
'Rotated Text'); end;
end; until MountEntry = nil;
end; // Close the file.
endmntent(MntDrives);
end;
Figure 1: Using OnPaint to draw rotated text.
Figure 3: This procedure fills a ListView control with information
about the mounted drives.

Figure 4: ListView control loaded with drive information.


Figure 2: Rotated text.
root directory — designated by a forward slash (/) — by a process
called mounting. When you have a new drive or a new partition,
To obtain the new position of the point, use the QPainter_xFormDev you mount it on the file system for it to become available. When
procedure. This procedure converts a point in device coordinates to a you don’t want to use it anymore, you can unmount it. It’s then
point in model coordinates, which can be used to draw the text. With disconnected from the directory tree, and becomes unavailable.
this knowledge, we can draw the rotated text in the middle of the form,
just as we did in the December 2000 article. Many physical devices reside in the /dev directory. The IDE drives,
for example, have names such as /dev/hda (first IDE drive), /dev/hdb
The OnPaint event handler for the form is shown in Figure 1. (second IDE drive), and so on. One physical disk can have many
Our base point is the middle of the form obtained from the ClientWidth logical partitions. These are usually numbered like this: /dev/hda1
and ClientHeight properties. Then we rotate the canvas 18 times (by (first partition in the first drive), /dev/hdb5 (fifth partition in the
20 degrees each time), and draw the text. After 18 rotations, we’ll have second drive), etc. These physical devices are mounted somewhere on
rotated 360 degrees and returned to the starting point. Each time before the directory tree, and are accessed like any other file.
drawing the text, we must transform the device coordinates in DevPoint
to the model coordinates in PrPoint, using QPainter_xFormDev. Then When Linux boots up, it searches the disk information in the fstab file,
we draw the text as we would normally. Figure 2 shows the result. located in the /etc directory. This is a text file, with a one-line description
of each drive to be mounted. Here’s an example of its contents:
Detecting Installed Drives
An article (“Drives, Files, etc.”) in the March 2001 issue of Delphi /dev/hda5 swap swap defaults 0 0
Informant Magazine showed how to detect installed drives on the /dev/hda6 / ext2 defaults 1 1
/dev/hda2 /boot ext2 defaults 1 2
Windows platform. Clearly, the same techniques won’t work in
/dev/fd0 /mnt/floppy auto user,noauto 0 0
Linux, which doesn’t even have the concept of a drive. /dev/cdrom /mnt/cdrom iso9660 user,ro,noauto 0 0
none /proc proc defaults 0 0
Linux has a file system similar to the directory tree in Windows. none /dev/pts devpts gid=5,mode=620 0 0
To make anything available to the user, you must include it in the /dev/hda1 /mnt/win vfat exec,dev,suid,rw 1 1

38 June 2001 Delphi Informant Magazine


At Your Fingertips

_statfs = record
f_type: Integer; // Type of filesystem.
f_bsize: Integer; // Optimal transfer block size.
f_blocks: __fsblkcnt_t; // Total data blocks in fs.
f_bfree: __fsblkcnt_t; // Free blocks in fs.
f_bavail: __fsblkcnt_t; // Free blocks available.
f_files: __fsfilcnt_t; // Total file nodes in fs.
f_ffree: __fsfilcnt_t; // Free file nodes in fs.
f_fsid: __fsid_t; // File system ID.
f_namelen: Integer; // Maximum length of file names.
f_spare: packed array[0..6-1] of Integer;
end;
TStatFs = _statfs;
PStatFs = ^TStatFs;

Figure 5: The TStatFs structure. Figure 7: ListView with mounted drives, total, and available space.

procedure TForm1.FormCreate(Sender: TObject);


The fifth column is used by the dump command to determine
var which file systems need to be dumped.
MntDrives : PIOFile; The sixth column is used by fsck to determine the order of file
MountEntry : PMountEntry;
Buf : TStatfs;
system checks done at reboot time.
begin
// Open /etc/mtab. Once the drives are mounted, a list of current mounted drives is
MntDrives := setmntent('/etc/mtab', 'r'); maintained in the /etc/mtab file, which can be accessed to inspect
if MntDrives <> nil then
// Process drives until there are no more. mounted drives. To read this list, you can use the getmntent function.
repeat Before using it, you must open the file with the setmntent function.
// Get next entry. This function is declared like this:
MountEntry := getmntent(MntDrives);
// Found?
if MountEntry <> nil then function setmntent(__file: PChar; __mode: PChar):
with MountEntry^ do PIOFile; cdecl;
// Add one line for each drive entry found.
if statfs(mnt_dir,Buf) >= 0 then
// Add only if block count is > 0. You must pass the file name and opening mode to it. This mode is
if Buf.f_blocks > 0 then the same used in the fopen function. For example, to open it in read
with ListView1.Items.Add do begin mode, we pass r as this parameter. The function returns a PIOFile (a
Caption := string(mnt_fsname);
SubItems.Add(string(mnt_type));
pointer to a file), which can be used in the getmntent function. This
SubItems.Add(string(mnt_dir)); function is declared like this:
// Add available space in Kb.
SubItems.Add(IntToStr(Buf.f_blocks *
function getmntent(__stream: PIOFile): PMountEntry; cdecl;
Buf.f_bsize div 1024));
SubItems.Add(IntToStr(Buf.f_bavail *
Buf.f_bsize div 1024)); You pass the PIOFile returned by setmntent. It returns a PMountEntry
end; (a pointer to a TMountEntry structure) pointing to the next mounted
until MountEntry = nil;
// Close the file. drive, or nil if there are no more mounted drives. TMountEntry is a
endmntent(MntDrives); structure containing one line of the file, separated into its respective
end; fields. It’s declared like this:

Figure 6: List mounted drives with total and available space. mntent = record
mnt_fsname: PChar; // Device for file system.
mnt_dir: PChar; // Directory mounted on.
Each column can be separated by space or tab characters. Each line
mnt_type: PChar; // Type of filesystem.
contains the following information: mnt_opts: PChar; // Options for fs.
The first column is the name of the physical device to be mnt_freq: Integer; // Dump frequency (in days).
mounted. The first line in this example shows /dev/hda5, the mnt_passno: Integer; // Pass number for 'fsck'.
fifth partition of the first IDE drive. end;
TMountEntry = mntent;
The second column is the device mount point, the directory PMountEntry = ^TMountEntry;
of the device to be merged. For example, /dev/hda1 will be
mounted in /mnt/win. All subdirectories from this partition will
be subdirectories of /mnt/win. When you’ve finished using the file, you must call endmntent to close
The third column is the file system type. In this file we can see the it. The code in Figure 3 shows how to retrieve the drive information
swap type, indicating a Linux swap partition; the ext2 type, a Linux and fill a ListView control with the mounted drives.
file system with long file names; the iso9660 type, used in CD-
ROMs; and the vfat type, used with Windows Fat32 partitions. We open the file /etc/mtab with the function setmntent. It returns a
The fourth column shows the device mount options. Here you PIOFile, stored in the MntDrives variable. Then we loop through all
can have many options, separated by commas. For example, the drive entries using getmntent. When there are no more drives, getmntent
CD-ROM has the options user, ro, and noauto, indicating that returns nil. If the return value isn’t nil, we add a new item to the
the user can mount this device, it must be mounted read-only, ListView, filling it with the MountEnt structure values. At the end, we
and that it shouldn’t be mounted automatically at boot time. close the file with endmntent. Figure 4 shows an example of the results.

39 June 2001 Delphi Informant Magazine


At Your Fingertips
Determining Disk Total and Available Space
If you want to know the available space on some disks in Windows, you
can use Delphi’s DiskFree function, from its SysUtils unit. This function
returns an Int64 with the available space on the requested drive.

DiskFree isn’t implemented in Linux. To retrieve the same information in


Linux, we must use the standard C library function, statfs. This function
returns information about a mounted file system, and is declared thus:

function statfs(__file: PChar; var __buf: TStatFs):


Integer; cdecl;

The first parameter is the file name of any file within the mounted
file system. You can even use the path name where the device was
mounted. The second parameter is a reference to a TStatFs structure
(see Figure 5). This structure will be filled with information when
the function returns.

The members that interest us are f_bsize, which shows the block size
for the device; f_blocks, which shows the total blocks; and f_bavail,
the available blocks in the device. To retrieve the available space in
all mounted drives, we’ll scan the /etc/mtab file as we did in the last
tip, and pass the mount point of every mounted device as the first
parameter to the statvs function. The code in Figure 6 shows how
to retrieve information about all mounted drives, and load the data
into a ListView control.

The code is similar to that of the last tip. For each drive we find
in /etc/mtab, we pass the mount point to statvs. If the number of
total blocks is greater than 0, we have a valid device and add it to
the ListView. The space is obtained by multiplying the number of
blocks by the block size, then dividing by 1,024, to display the size in
KB. Figure 7 shows the ListView with the mounted devices and their
respective total and available space.

Conclusion
We’ve seen how to use the QPainter class to draw text at many angles.
When Delphi 6 is available, you will be able to use the code for
this tip in Delphi and Kylix, just by recompiling the source code.
Even though we’re accessing capabilities not directly supported by the
VisualCLX TCanvas, the code is still portable.

The two drive-related tips, however, cannot be ported to Windows,


because they use functions from the standard C library available only
in Linux. When porting software from Windows to Linux — or vice
versa — you must be careful regarding which API, or library, etc. you’re
using, and make good decisions about what needs to be portable. ∆

Projects demonstrating the techniques described in this article are avail-


able on the Delphi Informant Magazine Complete Works CD located in
INFORM\2001\JUN\DI200106BS.

A Brazilian, Bruno Sonnino has been developing with Delphi since its first version
in 1995. He has written the books 365 Delphi Tips, Kylix - Delphi for Linux, and
Developing Applications in Delphi 5, published in Portuguese. He can be reached
at sonnino@netmogi.com.br.

40 June 2001 Delphi Informant Magazine


New & Used
By Clay Shannon

GExperts
The Indispensable Open-source Delphi Add-in

Y ou can live without GExperts, but after you use it, you won’t want to. Many of you
already agree with me, as GExperts won Delphi Informant Magazine’s Readers
Choice award for Best Open Source Product (see DI’s April 2001 issue). Quoting from
the introduction of the GExperts Web site: “GExperts is a set of tools built to increase
the productivity of Delphi and C++Builder programmers by adding several features to
the IDE.” That’s an understatement. GExperts makes a great deal of tedious coding a
snap, and enhances our favorite development tool in myriad ways.

I don’t use many third-party tools, preferring to stick Twenty-five Experts


with what came “in the box” and my own compo- The Procedure List displays the procedures in the
nents. But my curiosity about GExperts eventually current unit. You can filter based on object, per-
got the better of me after hearing comments from form an incremental search, and double-click on
colleagues such as “It’s a must have,” and “You don’t the procedure to go to it in the code editor. Differ-
have GExperts installed?” These remarks were made, ent icons are shown to differentiate between stand-
not in a matter-of-fact way, but incredulously, as if alone procedures and functions, procedures and
they were asking “You don’t like pizza?” functions that are members of objects, construc-
tors, and destructors.
GExperts is a programmer’s dream: It’s an array of
useful experts that work well and save you time. The Expert Manager displays all the experts cur-
They’re also visually pleasing, not ugly or plain like so rently registered with Delphi, and allows you to
many tools made for programmers. GExperts exhib- easily enable, disable, or remove any of them.
its the perfect combination of ease of use and power
— certainly a good fit for Delphi in that regard. The Grep expert is similar to Delphi’s Find func-
Additionally, the price is right — it’s a free download, tionality (Search | Find In Files), but is more full-fea-
and now an open source project, allowing everyone tured (and faster). You can search all the files in the
the opportunity to enhance or extend it. project, just the open project files, or search in the
current file only or specific directories (in which
If these reasons aren’t enough to use GExperts, case there are handy speed buttons for searching
note that the installation is as smooth as silk. Delphi’s rtl or vcl directories). The Grep Search
Simply download the latest version (1.0 at the expert shows the results of the Grep search, divided
time of this writing), double-click setup.exe, and into the various units in which the searched expres-
everything is done for you. The next time you sion was found (see Figure 1). Additionally, the
start Delphi, your menu bar will sport a new top phrase for which you searched is highlighted.
menu item of GExperts.
The Message Dialog expert (see Figure 2) makes
Common to most of the experts in GExperts is a the creation of MessageDlg calls easier than
Help (question mark) speed button. It takes you ShowMessage (which some resort to because they
to the corresponding help topic, which is helpful don’t want to take the time to use the more profes-
in those instances when you want a quick overview sional-looking MessageDlg). You simply place your
of what the expert does, or when you want more cursor where you want the call to go, select the
detailed information than you can immediately options you want in the Message Dialog expert,
grok from “playing” with a particular expert. and voilà! When you select the Test button, you’ll

41 June 2001 Delphi Informant Magazine


New & Used

Figure 2: The Message Dialog expert.


Figure 1: The Grep Search expert.
The Source Export expert (say that three times fast!) is an excellent
see the dialog as it will appear at run time (see Figure 3), and the tool for formatting source code for documentation purposes, and
necessary MessageDlg statement will be generated for you. retaining syntax highlighting for readability. You can configure the
colors of the various elements (which won’t necessarily be the same
The Backup Files expert adds files in the current project to a list, combination you want onscreen, if you want to print the code or
which you can edit. Then, you simply select the Backup button, and display it on a Web page), and save as either .htm/.html or .rtf.
a .zip file is created in the same directory as the project. By default,
only files with an extension of .pas, .dfm, .dpr, and .res are included, The Code Librarian is a repository for code snippets to which you
but you can easily include others by adding a wildcard. You now have want easy access. Before now, I had pieces of code stuck here and
no excuse for not backing up your projects on a regular basis. there in directories named things like “goodcode,” and I often had
to search my hard drive for a *.pas file that had a function named
Set Tab Order allows you to set the tab order of controls on a form. “<whatever>”. Code Librarian allows you to organize your treasure
First, V-select the controls for which you want to set the tab order, and trove of code snippets for quick reuse. You can download a starter
they’ll appear in the list in the order in which you selected them. Simply kit of code snippets from the GExperts site (http://www.gexperts.org/
click OK, and the tab order property for each control is set accordingly. download.html), and insert your own code snippets by adding folders
and snippet files within them.
The Clean Directories expert allows you to rid directories of the
by-product files that pile up while developing in Delphi. These The ASCII chart is self-explanatory, while the PE Information expert
ancillary files not only take up space, but also make the important allows you to examine various pieces of information about the con-
files harder to identify. For example, you can rid projects of all files tents of executables, DLLs, and packages.
with extensions beginning with a tilde. This expert is extensible so
you can add custom file extensions to be “cleaned.” After you select The Replace Components expert allows you to easily replace TButtons
the Clean button, a message informs you of how many files were with TBitBtns, or vice versa, or with any other appropriate component
deleted, and how much disk space was recovered. (see Figure 4). This is very handy when a project-wide GUI change is
necessary. And it’s a real nuisance without GExperts.
Now I’d like to take the opportunity to make an enhancement
request to the Clean Directories expert: Wouldn’t it be nice to have a The Component Grid expert shows all the components on a form
matching pair of “Select All” and “Clear All” buttons so that all the and their parents, as well as their Tag and HelpContext properties. The
extensions could be selected at once, or you could start over, if neces- latter two properties are editable (one-stop property setting).
sary, by deselecting them all? Oops! I just remembered. GExperts is
an open source project, so by complaining I’ve obligated myself to The IDE Menu Shortcuts expert allows you to set your own menu
look into adding this functionality. Unless somebody beats me to it! shortcuts for all of Delphi’s menu items. The Project Dependencies
expert shows, on a unit-by-unit basis, which units are used, and which
The Clipboard History expert does just what it says (of course, it’s other units use the current one. Indirect dependencies are also shown.
only active while Delphi is running). It spans multiple sessions, and
you can clear the entries when you want. The number of entries The Perfect Layout expert helps you arrange the IDE so that
to retain in the history are set via GExperts Configuration | Experts | screen real estate is used as efficiently as possible without overlap.
Clipboard History | Configure | Maximum Entries. You can select one of two pre-configured arrangements, or create
your own custom arrangement. Selecting Perfect Layout from the
The Favorite Files expert provides a convenient way to organize GExperts menu in Delphi activates the currently configured layout.
commonly used files, such as bitmaps for glyphs and help files. To specify the desired layout, first select GExperts | GExperts Con-
The Class Browser is a supplementary utility to Delphi’s browser figuration, then the Configure button corresponding to the Perfect
(accessible via View | Browser). Layout expert you want.

42 June 2001 Delphi Informant Magazine


New & Used

GExperts is an indispensable open-source Delphi add-in. Any one


of its 25 experts make it worth the download. It installs easily, is
reliable, attractive, and free. So what are you waiting for?

GExperts
Figure 3: Select the Test button to get this dialog box. Web Site: http://www.gexperts.org
Price: Open source (free).

The ToDo List expert is a replacement for Delphi’s To-Do List. You
can categorize to do items by level of importance/severity. To add new this is the Editor expert, which provides hotkeys to perform actions
to-do items, you should type comments in your code like this: such as commenting and uncommenting code, adding the date (in
whatever format you specify), and locating and matching delimiters.
{ #ToDo1 Rewrite this code so that it actually works. }
{ #ToDo2 Add support for XML here later. } Conclusion
What are you waiting for? Download GExperts now. It’ll save you
The Code Proofreader allows you to specify that certain typos time and money, make you look good, and maybe even help you win
are automatically corrected. For friends and influence people. And who knows — you may be the
example, you can replace “:+” next person to enhance or extend this programming masterpiece. ∆
and “;+” with “:=”.

The Priority Booster expert sets


compilation priority. However,
it didn’t seem to work in the
pre-release version.

The Project Option Sets expert


allows you to set default project
and environment settings, Figure 4: The Replace Compo-
create named options, and nents expert.
much more.

The Components To Code expert copies the necessary code to the


Clipboard so you can programmatically create the highlighted con-
trol. For example, placing a TBitBtn on a form and selecting this
expert copies the code in Figure 5 to the Clipboard.

Of course, your property values will vary depending on what you


name the control, where you drop the button, etc.

The GExperts Configuration expert, mentioned earlier in connection


with the Perfect Layout expert, allows you to configure which experts
are used, and to some extent, how they’re used. A very useful part of

var
btnAbout: TBitBtn;

btnAbout := TBitBtn.Create(Self);
with btnAbout do begin
Name := 'btnAbout';
Parent := Self;
Left := 8;
Top := 340;
Width := 75;
Height := 25;
Caption := '&About';
Clay Shannon is an independent Delphi consultant, author, trainer, and mentor
TabOrder := 0; operating as Clay Shannon Custom Software. He’s available for remote develop-
OnClick := btnAboutClick; ment or short-term on-site assignments in the Coeur d’Alene, ID/Spokane, WA
// Glyph := // please assign
NumGlyphs := 2;
area. He’s the author of Developer’s Guide to Delphi Troubleshooting (Wordware
end; 1999), which is soon to be revised. To view his resume, point your browser to
http://www.sysadminsrus.net/clayshannon/ClayShannon.doc. You can e-mail him
Figure 5: GExperts generates the code you need to program- at bclayshannon@earthlink.net.
matically create the highlighted control.

43 June 2001 Delphi Informant Magazine


Best Practices
Directions / Commentary

By Clay Shannon

Writing Pithy Code

T oo many programmers are like F. Scott Fitzgerald, or even Marcel Proust, when it comes to their coding style,
but more closely resemble Ernest Hemingway when it comes to commenting. They have the comment:code ratio
backward. Comments should be amply sprinkled throughout the code, and be well-thought-out enough to actually
make sense to other coders without intimate knowledge of the source. Courteous coders don’t scrimp on commenting.

Oddly, the code itself is sometimes verbose. It goes on and on, when | Syntax Options | Complete Boolean eval checked (by default it isn’t).
a simple, concise statement or two would do. We’ve already covered
the subject of commenting code in this column (in the May 2000 Applying the first two suggestions, if the result of the two tests is a
issue), so let’s now tackle the subject of writing concise code using Boolean assignment, you can simplify a statement like this:
five specific examples.
if YouLikeDelphi then
One. Boolean assignment statements should be assigned directly. if YouLikeLinux then
Result := True;
That is to say, if you have an if/then/else statement where the if sets
a Boolean to True, and the else sets it to False (or vice versa), you
can make your code cleaner by using a single assignment. In other to this:
words, instead of writing:
Result := YouLikeDelphi and YouLikeLinux;

if SomethinAintRaht then
Result := True In brief, there’s usually no purpose for Result := True or Result := False
else
assignments in your code when they’re the result of a conditional test. Of
Result := False;
course, at times you will make direct assignments to Boolean values, most
often when initializing variables. Even then, however, it’s unnecessary to
you can reduce the code from four lines to one by writing this: initialize Boolean values to False, as they are thus initialized anyway by
Delphi. (Boolean members of objects are auto-initialized to False (0),
Result := SomethinAintRaht; integers to 0, strings to an empty string (''), etc.)

Two. In most cases, you can avoid if/then/if statements by combining Three. It’s unnecessary to write “= True” or “= False” when testing
the two tests into an and condition. For example, instead of this: a Boolean value. Thus, this condition:

if YouLikeDelphi then if (HisCockamamieStory = True) and


if YouLikeLinux then (JohnnieBelievesIt = False) then
BuyKylix(Now);

is verbose. If the property or function is or returns a Boolean value,


you can do this: simply code it this way:

if YouLikeDelphi and YouLikeLinux then if HisCockamamieStory and


BuyKylix(Now); not JohnnieBelievesIt then

You don’t have to worry about the second half of the test being executed Four. Another common operation that’s made unnecessarily
if the first one fails, as long as you don’t have Project | Options | Compiler complex by some programmers is concatenating strings by using

44 June 2001 Delphi Informant Magazine


Best Practices
the + operator. And it’s inefficient to boot. For example, this
statement:

ShowMessage('X coordinate is: ' + IntToStr(X) +


' ' + 'Y coordinate is: ' + IntToStr(Y));

is better constructed thus:

ShowMessage(Format(
'X coordinate is: %d Y coordinate is: %d', [X,Y]));

Five. The with statement is not only more efficient, but also often
makes code more readable. For example, compare this:

if Sender is TEdit then


if (TEdit(Sender).Text = '') or
(TEdit(Sender).Text[TEdit(Sender).SelStart] = ' ') or
(TEdit(Sender).SelLength =
Length(TEdit(Sender).Text)) and
(Key in ['a'..'z']) then
Key := UpCase(Key);

with this:

if Sender is TEdit then


with Sender as TEdit do
if (Text = '') or
(Text[SelStart] = ' ') or
(SelLength = Length(Text)) and
(Key in ['a'..'z']) then
Key := UpCase(Key);

Although neither may be crystal clear at first blush, I think you’ll


agree that the latter is at least more readable, and certainly less
cluttered.

So there you have it. These five suggestions probably won’t knock
you off your feet with their insight, but they will certainly pro-
duce more readable — thus more maintainable — and efficient
code. ∆

Clay Shannon is an independent Delphi consultant, author, trainer, and mentor


operating as Clay Shannon Custom Software. He is available for remote develop-
ment or short-term on-site assignments in the Coeur d’Alene, ID/Spokane,
WA area. He is the author of Developer’s Guide to Delphi Troubleshooting
[Wordware, 1999], soon to be revised. To view his resume, point your browser to
http://www.sysadminsrus.net/clayshannon/ClayShannon.doc. You can reach him
at bclayshannon@earthlink.net.

Visit www.BorlandSolutions.com today!

45 June 2001 Delphi Informant Magazine


File | New
Directions / Commentary

UML and Design Patterns, Part One: The Classics

T his is the first of a two-part discussion of the conceptual tools that make it possible for us to plan projects and
reuse common patterns during the development phase. The two approaches that seem to have captured the
attention of the development community are UML (Unified Modeling Language) and design patterns.

It’s been several years since I first heard about UML and design oriented development using a specific application — a word proces-
patterns. While enjoying lunch with several new friends at a Borland sor. This is very helpful and accomplishes a couple of important
Conference (I think it was the one in Nashville), others at the table objectives. First, it demonstrates in a concrete manner different con-
were wondering when there might be sessions on these topics at texts in which various patterns can be used. Second, it provides a lot
a Borland conference. We didn’t have to wait long. For the past of clues about making programming decisions using these patterns.
two years there have been a number of workshops and tutorials, The second part of the book discusses 23 patterns that fall into three
particularly on design patterns. All the ones I have attended have general categories: creational, having the responsibility of instantiat-
been outstanding, particularly those given by Mark Miller, who has ing classes; structural, providing various means of extending a class’
a knack for relating design patterns to programming tasks in general, functionality; and behavioral, concerned with a class’ specific tasks
and Delphi in particular. and areas of responsibility. Let’s briefly examine a few patterns to get
a better idea of how useful they can be in planning and writing an
Last summer after attending one of Mark’s sessions it occurred to me application.
that it was about time I learned more. This seemed particularly timely
since I was starting to work again on updating a large educational Consider the Singleton, a unique pattern whose purpose is to make
application and needed some conceptual tools to help make the certain that a class has one and only one instance. It has the addi-
whole job more manageable. I did an Internet search to see what tional responsibility of providing a global point of access to the
books were available, selected quite a few, and have read or at least instantiated class. One obvious place for using the Singleton would
heavily browsed all of them. In this column I will share with you my be in storing application-state data, so that when a user starts an
assessment of these works in my ongoing self-education. This month application it has the same visual appearance and data loaded as the
I will discuss the primary sources, to which I refer as “Classics,” even last time it was shut down. At the opposite end of the spectrum is the
though they were written in the mid-1990s. Next month I’ll discuss Flyweight, a pattern used to support large numbers of closely related
the secondary sources under “New Trends.” objects (such as character objects in a word-processing application)
with a minimum of overhead. This pattern can be used in many
Design patterns. Design patterns are intimately related to object- different contexts at the same time.
oriented programming. They’re based upon the recognition of certain
facts — facts that are quite subtle, but obvious once we realize them: There are patterns for creating families of objects (Abstract Factory),
1) that the objects we use in building a program can be very different creating new objects from a basic one (Prototype), and creating an
from each other in their fundamental purpose and design; and 2) we object that defers the instantiation of a specific class to its subclasses
might easily use the same or similar designs over and over without (Factory Method). This work presents each pattern in a very under-
realizing it. The realization of these facts by many developers during standable manner, with its purpose(s), its synonyms, its use in solving
the 1990s led to an interest in identifying these patterns. In 1995 programming problems, its constituents, its structure, and so forth.
this activity resulted in the publication of the seminal work on the The authors take great pains to make each pattern as understandable
topic, Design Patterns: Elements of Reusable Object-Oriented Software as possible.
[Addison-Wesley, ISBN 0-201-63361-2] by Erich Gamma, Richard
Helm, Ralph Johnson, and John Vlissides (who have since become Unified Modeling Language. Design patterns are of interest to devel-
known as “the gang of four”). opers, but probably no one else. The UML is different. Not only can
this tool be used to help us in modeling our software, it can also
Design Patterns discusses a great deal more than patterns. The first be used to communicate design decisions to others — our managers,
part of the book discusses the relationship of these patterns to object- clients, colleagues, etc. This should be obvious since models provide

46 June 2001 Delphi Informant Magazine


File | New
a means of connecting an application with the specific domain in classes, behavioral elements such as a series of states, grouping things
which it is to operate. The UML does have one strong similarity that show relationships, and annotational elements that act as com-
to design patterns: it’s also highly involved with objects and classes. ments. The four kinds of relationships supported in the UML are
And well it should be. After all, one of the great strengths of object- dependency, association, generalization, and realization. Finally, the
oriented programming is that it enables us to devise programming UML consists of a rich assortment of diagrams for describing classes,
elements (objects) that mimic real-world objects. UML is simply an objects, event sequences, collaborations, and much more.
extension of this approach.
The three books I have discussed here are the essential resources for
The classic works in UML were written by James Rumbaugh, anyone who wishes to use design patterns or the UML, but there are
Ivar Jacobson, and Grady Booch, who have become known affection- other worthwhile books we’ll examine next month. Among these is a
ately as the “three amigos.” We’ll examine the two essential works, new book by the “three amigos.” Others have found interesting ways
The Unified Modeling Language User Guide [Addison-Wesley, 1998, of extending, modifying, and combining these approaches. We’ll also
ISBN 0-201-57168-4], and The Unified Modeling Language Reference explore a number of other books, delving further into the nuances
Manual [Addison-Wesley, 1998, ISBN 0-201-30998-X]. The first is and implications of these important new tools. Until then. ∆
an introduction to UML, the latter a detailed reference to the elements
of this complex language. In a sense, these two books are analogous — Alan C. Moore, Ph.D.
and serve a similar purpose to the two main sections of the design
patterns book we examined previously. In both cases, you should begin Alan Moore is a Professor of Music at Kentucky State University, specializing in music
with the first part (or book), developing a familiarity with the topic. composition and music theory. He has been developing education-related applications
Then, as you begin to create and work with it, use the second part (or with the Borland languages for more than 15 years. He is the author of The Tomes
book) as a handy reference to answer questions that come up. of Delphi: Win32 Multimedia API [Wordware Publishing, 2000] and co-author (with
John Penman) of an upcoming book in the Tomes series on Communications APIs. He
Some of the terms and concepts used in UML have been around has also published a number of articles in various technical journals. Using Delphi, he
for a while and are familiar to most developers. For example, use specializes in writing custom components and implementing multimedia capabilities
cases are a central element in the language. The language consists of in applications, particularly sound and music. You can reach Alan on the Internet at
three obvious parts: things (I would prefer a more sophisticated acmdoc@aol.com.
term like entities or items, but “things” does communicate a lot),
relationships, and diagrams. Things include structural elements like

47 June 2001 Delphi Informant Magazine

You might also like