Beruflich Dokumente
Kultur Dokumente
ON THE COVER
25
34
FEATURES
9
In Development
REVIEWS
Sound + Vision
19
Dynamic Delphi
At Your Fingertips
38
14
Greater Delphi
42
DEPARTMENTS
2
43
Delphi Tools
File | New by Alan C. Moore, Ph.D.
Delphi
T O O L S
New Products
and Solutions
Book Picks
Securing Windows
NT/2000 Servers
Stefan Norberg
OReilly
ISBN: 1-56592-768-0
Cover Price: US$29.95
(199 pages)
ISBN: 0-596-00002-2
Cover Price: US$39.95
(684 pages)
Delphi
T O O L S
New Products
and Solutions
Book Picks
Red Hat Linux 6
Harold Davis
Peachpit
DbCAD dev: OCX and ActiveX Components for GIS, MAPPING, GPS, and CAD Applications
ISBN: 0-201-35437-3
Cover Price: US$29.99
(311 pages, CD-ROM)
Applying COM+
Gregory Brill
New Riders
ISBN: 0-7357-0978-5
Cover Price: US$49.99
(466 pages)
Delphi
T O O L S
New Products
and Solutions
Book Picks
JavaScript for the World
Wide Web, 3rd Edition
Tom Negrino and Dori Smith
Peachpit
ISBN: 0-471-39992-2
Cover Price: US$29.99
(210 pages)
EC Software Releases EC
Software Help Suite
EC Software released EC Software Help Suite (EHS) for Delphi
3, 4, and 5. EHS covers every help
related task in a Delphi application and completes the help functions that Delphi provides.
EHS implements a full-featured
context-sensitive help system.
Without a single code line it provides a right-click Whats This?
menu item for every control and
adds a Whats This? button
to the main form, similar to
the help function of Microsoft
Word. It also implements rightclick help for menu items and
gives you full control over the
1 key. EHS builds a bridge
to HTML HELP and it HTML
HELP-enables your applications;
it also supports HTML HELP
pop-up topics, as well as mixedmode systems, a combination of
HTML HELP and WinHelp.
EC Software Help Suite comes
with full source code. A demonstration application that illustrates the features is included.
EC Software
Price: Free
Contact: info@ec-software.com
Web Site: http://www.ec-software.com/
comppage.htm
By Bill Todd
dbExpress
SQL Database Access for Kylix and Delphi 6
bExpress is Borlands new database access technology that will make its first appearance in Kylix (Delphi for Linux) and Delphi 6. Although its been widely referred to as
a replacement for the Borland Database Engine (BDE), this is not entirely true.
dbExpress is a new set of components for accessing
SQL databases. You wont be able to access Paradox
and dBASE tables using the dbExpress drivers supplied by Borland because those database management systems are built into the BDE. If you want to
use Paradox as your database, youll have to continue
to use the BDE. However, as youll see later in this
article, this may not be the case for dBASE users.
[Note: Everything in this article applies to both
the Windows and Linux platforms unless otherwise
noted. Since this article is based on pre-release versions of dbExpress, some features may change in
the nal version.]
Borland designed dbExpress with the following
goals in mind:
Make it small.
Make it fast.
Make it cross-platform.
Avoid the installation and conguration
problems of the BDE.
Make it easy for anyone to write dbExpress
drivers.
Windows File
Linux File
Description
DBEXPINT.DLL
(116KB)
LIBSQLIB.SO
MIDAS.DLL
(270KB)
MIDAS.SO
GDS32.DLL
(339KB)
LIBGDS.SO
Figure 5: The
SQLConnection
component
Params
property.
Conclusion
the user interface, add a DataSource component and set its
DataSet property to the ClientDataSet, and then connect your
data-aware controls to the DataSource. As you add, delete,
and modify records, the changes are stored in memory in the
ClientDataSet components Delta property. When you call the
ClientDataSet components ApplyUpdates method, all of the accumulated changes are sent to the DataSetProvider, which generates
SQL statements to update the tables in the database.
The trick here is that the ClientDataSet holds all of the records
returned by the query in memory, so you need to use traditional
client-server architecture for your application, where the user must
enter selection criteria that allows you to fetch a reasonably small
result set. This may sound restrictive at rst, but once you explore
the powerful features of the ClientDataSet component, youll never
want to write a database application any other way. Besides, holding records in memory is not as big a limitation as it sounds,
when you consider that 10,000 records that are 100 bytes long will
only consume 1MB of memory and 100,000 records will consume
10MB. While you would not want to load that many records into a
ClientDataSet, in most situations you certainly can if necessary.
Bill Todd is president of The Database Group, Inc., a database consulting and
development firm based near Phoenix. He is co-author of four database programming books, author of more than 60 articles, a contributing editor to Delphi
Informant Magazine, and a member of Team Borland, providing technical support
on Borland Internet newsgroups. He is a frequent speaker at Borland Developer
Conferences in the US and Europe. Bill is also a nationally-known trainer, and has
taught Delphi programming classes across the country and overseas. Bill can be
reached at bill@dbginc.com.
In Development
Debugging / Delphi 5
orrecting errors in code is a necessary but all-too-often frustrating and timeconsuming task faced by software developers. Fortunately for Delphi developers,
Delphis integrated debugger includes a wealth of features that greatly assist bug searchand-destroy missions.
Last month, Part I of this series introduced you to
a number of the support tools provided by Delphis
debugger. This month, well take an in-depth look
at the workhorse of code debugging breakpoints.
There are four types of breakpoints: source breakpoints, address breakpoints, data breakpoints, and
Module load breakpoints. Each is described in the
following sections.
Source Breakpoints
Figure 1: Blue diamonds indicate which lines are compiled, and to which source
breakpoints can be attached.
9 March 2001 Delphi Informant Magazine
There are a number of ways to add a source breakpoint to your code. The easiest is to click the gutter
of the code editor to the immediate left of the
line to which you want to attach a breakpoint.
In Development
The dened breakpoint appears in the editor as a large red dot by
default (in place of the smaller blue diamond). Also, the entire line is
highlighted in red, or whatever alternative color youve dened using
the Color page of the Editor Options dialog box.
Defining Conditions
You can use the Condition text box to enter an expression to control
whether the breakpoint will trigger. It can be any valid Boolean
expression, including functions, constants, variables, operators, and
so forth. When the Boolean expression evaluates to True, the breakpoint triggers; otherwise, it does not.
The only limitation to the conditional expression is that all symbols used within the expression must be within scope of the
breakpoint. For example, the value of a local variable of a method
can be used in the condition of a source breakpoint, but only
if that breakpoint appears within the method in which the local
variable is declared. Likewise, a function can only be used in the
expression if the breakpoint is placed within a unit that uses the
functions unit. For example, you can use AnsiCompareText in the
Condition text box, but only if the unit in which the breakpoint
appears is declared to use the SysUtils unit (the unit in which
AnsiCompareText is declared).
In Development
Figure 3 (Top): You can see the current pass count, as well as the pass count condition, from
the Breakpoint List window. Figure 4 (Bottom): The Breakpoint List window includes breakpoint
group membership.
Breakpoint Groups
Breakpoints can be organized into groups. You designate a breakpoint
as a member of a breakpoint group when you want to enable or disable all breakpoints associated with the group at run time. Enabling
or disabling breakpoint groups is discussed later in this article.
To assign a breakpoint to a group, enter the group name in the
Group combobox of the Source Breakpoint Properties dialog box.
Alternatively, if you want to assign the breakpoint to a group to
which you have previously assigned a breakpoint, select that group
name from the Group combobox.
The group to which a particular breakpoint has been assigned
appears in the Breakpoint List window, as shown in Figure 4. This
window lists ve breakpoints. Three of these belong to one of two
different groups: click or wiggle.
Breakpoint Actions
Some of the more interesting things you can do with breakpoints
can be controlled with a breakpoints action properties. To display
the breakpoint action properties, click the Advanced button on the
Source Breakpoint Properties dialog box. This expands the dialog
box, as illustrated in Figure 5.
In Development
tion line, or right-click
the instruction line and
select Toggle breakpoint.
A breakpoint appears
as a large red dot in
the Disassembly pane
gutter, as shown in the
Figure 6.
of that expression to the event log using the Log result checkbox.
This checkbox, which is enabled by default, is useful when you
want to execute one or more functions that produce side effects
from within the expression, but dont actually need the value
of the resulting expression to be written to the event log. For
example, you may execute a function in the expression that serves
to destroy some temporary les created by some other part of your
application, ignoring any value returned by that function.
The last two comboboxes permit you to enable or disable entire
groups of breakpoints within your application. To disable a group
of breakpoints, enter the name of the group whose breakpoints
you want to disable in the Disable group combobox. Alternatively,
to enable a group of breakpoints, enter the name of that group
into the Enable group combobox. A single breakpoint can both
enable one group and disable another group when triggered.
Controlling which breakpoints are enabled is a powerful action for
a non-breaking breakpoint. For example, imagine the example presented earlier where a code segment should execute once, and once
only. If you determine that the code is executing more than once, you
might consider placing a number of breaking breakpoints within that
code segment, assigning them all to the same group. Initially, these
breakpoints could be disabled, since these are expected to be executed
once, at a minimum. You then place one non-breaking breakpoint
at the rst line of the code segment, setting its Pass count to 2 and
using its action to enable the breakpoint group. When the code
segment is executed the second time, the breakpoints in the group
become enabled, permitting you to use the debugger to determine
what conditions exist that permitted the code to execute again.
As mentioned earlier, there are four types of breakpoints, with the
source breakpoint being the most commonly employed. The three
remaining breakpoint types are described in the following sections.
Address Breakpoints
Data Breakpoints
Data breakpoints are
those that trigger when
a particular memory
address is written to.
The memory address
Figure 8: The Add Data Breakpoint
dialog box.
can be represented
either as a hexadecimal
number, or as the variable used to refer to that memory address. Unlike other breakpoint types, which can persist from one Delphi session to the
next, data breakpoints last only for the current Delphi session.
(Conguring Delphi to persist breakpoints is described in the
nal section of this article.)
To add a data breakpoint, invoke the debugger (by either raising an
exception or triggering a breaking breakpoint). Then select Run | Add
Breakpoint | Data Breakpoint. Delphi responds by displaying the Add
Data Breakpoint dialog box shown in Figure 8.
Address breakpoints permit you to dene a breakpoint that will trigger when an instruction at a particular memory address is executed.
There are two ways to add address breakpoints, and both of them
require the debugger to be loaded. The rst technique involves the
Disassembly pane of the CPU window, which is the top-left pane
shown in Figure 6. Normally, you display the CPU window by
selecting View | Debug Windows | CPU.
To add an address breakpoint, click in the left gutter of the Disassembly pane in the CPU window on the line associated with
the instruction address, press Cb with your cursor on the instruc-
In Development
Figure 9: The Add
Module dialog box.
Enter the name of the module in the Module Name text box and
click OK. This is the only way to set a Module load breakpoint on a
module that isnt already loaded.
You can also create a Module load breakpoint from the Modules
window, but this is only useful if the project is in the debugger and
the module has already been loaded. With the debugger loaded, open
the Modules window by selecting View | Debug Windows | Modules
to view the window shown in Figure 10. Then, in the Module pane,
right-click the module you want to break on, and select Break On
Load. Modules that will trigger a Module load breakpoint appear in
the Module pane with a red dot to their left, as shown in Figure
10. To remove a Module load breakpoint, right-click the specic
module and select Break On Load again. (You can also add a Module
load breakpoint from the Modules window by right-clicking in the
Module pane and selecting Add Module.)
Then, right-click on the watch entry and select the Break on change
option. You can modify the breakpoint properties of data breakpoints
by right-clicking on the breakpoint in the Breakpoint List window
and selecting Properties.
Conclusion
Breakpoints are the most commonly used feature of Delphis integrated debugger. Unlike their name suggests, however, not all breakpoints break. In fact, as shown in this article, some of the most
valuable breakpoints you can set trigger without invoking the integrated debugger.
Sound + Vision
ast month, we examined the process of recording sounds to memory. Certain structures that are needed to record and play sounds were also discussed. Now its playback time, so well be examining the WaveOut functions (all of which begin waveOut...),
just as we examined the WaveIn functions.
Just as with the WaveIn functions, the WaveOut
functions generally return a code indicating success
or failure. And as you would expect, the functions
must be called in a specic order. Figure 1 describes
many of the WaveOut functions in the order in
which theyre usually called.
The WaveOut functions also use many of the same
parameters as the WaveIn functions. Heres a short
description of the parameters:
hWaveOut: A UINT identifying the waveform
output device.
lpCaps: The address of a WAVEOUTCAPS
structure to be lled with the capability
information of the device specied by
hWaveOut.
uSize: An integer (UINT) indicating the size
of the previous parameter. If that parameter
is a WAVEOUTCAPS structure, you should
use SizeOf(WAVEOUTCAPS) to get this value.
lpText: A PCHAR pointing to a text buffer that
holds an error message.
lphWaveOut: A (PHWaveOut) handle identifying the open waveform audio output device.
This handle is set by waveOutOpen, and
should be saved in a variable that can be used
by other waveform output functions when they
are called.
Sound + Vision
Function
Parameters
Description
waveOutGetNumDevs
Returns UINT
None
waveOutGetDevCaps
Returns MMRESULT
hWaveOut;
lpCaps; uSize
waveOutOpen
Returns MMRESULT
lphWaveOut; uDeviceID;
lpFormatEx; dwCallback;
dwInstance; dwFlags
waveOutPause
Returns MMRESULT
hWaveOut
waveOutPrepareHeader
Returns MMRESULT
hWaveOut;
lpWaveOutHdr;
uSize
waveOutWrite
Returns MMRESULT
lpWaveOutHdr; uSize
waveOutRestart
Returns MMRESULT
hWaveOut
waveOutStop
Returns MMRESULT
hWaveOut
waveOutReset
Returns MMRESULT
hWaveOut
waveOutUnprepareHeader
Returns MMRESULT
hWaveOut;
lpWaveOutHdr;
uSize
waveOutClose
Returns MMRESULT
hWaveOut
waveOutGetErrorText
Returns MMRESULT
lpText; uSize
Figure 1: WaveOut functions from the Microsoft Win32 application programming interface (API). The functions are declared for Delphi
in mmsystem.pas.
Sound + Vision
here. You can nd details of those in the Windows Multimedia Help File.
Or better yet, read Chapter 3 of my book, The Tomes of Delphi: Win32
Multimedia API [Wordware, ISBN: 1-55622-666-7]. That chapter is
devoted entirely to waveform audio and includes all of the functions.
The les referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103AM.
Now you can use these variables to iterate through the devices while using
waveInGetDevCaps and waveOutGetDevCaps to determine the properties
of each device. Store those properties in the records, FWaveInCaps and
FWaveOutCaps, accessing the various elds of those records to display
specic device properties. The code for displaying input and output
properties is shown in Listing Two (beginning on page 17).
Conclusion
You have come to the end of this exploration of working with waveform
input and output in memory using the double-buffering technique. The
explanations and examples presented here should be sufcient to get you
started working with waveform sound in memory.
There are many other details concerning the structures and functions
discussed here. There are also other waveform functions not discussed
16 March 2001 Delphi Informant Magazine
Sound + Vision
begin
FormatStruc.nChannels := FWaveOutCaps.wChannels;
FormatStruc.nSamplesPerSec := 22050;
end
else
begin
FormatStruc.nChannels := 2;
FormatStruc.nSamplesPerSec := 44100;
end;
FormatStruc.wFormatTag := WAVE_FORMAT_PCM;
FormatStruc.wBitsPerSample := 8;
FormatStruc.nBlockAlign := Round((FormatStruc.nChannels *
FormatStruc.wBitsPerSample) / 8);
FormatStruc.nAvgBytesPerSec :=
FormatStruc.nSamplesPerSec * FormatStruc.nBlockAlign;
FormatStruc.cbSize := 0;
CurrentMMResult := waveOutOpen(@FOutputHandle, DevNum,
(@FormatStruc), DWORD(@WaveOutProc),
DWORD(MainFormHandle), DWORD(callback_function));
Result := (CurrentMMResult=MMSYSERR_NOERROR);
end;
procedure TForm1.LoadNextDataBlock(
var PlaybackLength: DWORD; var TempHdr: PWAVEHdr);
var
TempBuffer: array[0..32767] of Byte;
begin
if FPlaybackBufferPosition<FMainBufferSize then
with TempHdr^ do begin
PlaybackLength :=
FMainBufferSize - FPlaybackBufferPosition;
if PlaybackLength>FBufferBlockSize then
PlaybackLength := FBufferBlockSize;
MainSoundBuffer.Read(TempBuffer, PlaybackLength);
Move(TempBuffer, lpData^, PlaybackLength);
FPlaybackBufferPosition := MainSoundBuffer.Position;
end
else
WaveOutStatus := wosDonePlaying;
end;
function TForm1.PlaySounds(var ResultMsg: string): Boolean;
var
j: Integer;
CurrentLength: Cardinal;
begin
Result := False;
CurrentLength := 0;
MainSoundBuffer.Position := 0;
FPlaybackBufferPosition := 0;
// Make sure you're at the start and not already playing.
if (WaveOutStatus=wosClosed) then begin
WaveOutStatus := wosPlaying;
waveOutPause(FOutputHandle);
// Iterate through two buffers.
for j := 0 to 1 do begin
LoadNextDataBlock(CurrentLength,
FWaveHeaderArray[j]);
FWaveHeaderArray[j].dwBufferLength := CurrentLength;
CurrentMMResult := WaveOutPrepareHeader(
FOutputHandle, PWaveHdr(FWaveHeaderArray[j]),
SizeOf(WAVEHdr));
if CurrentMMResult<>MMSYSERR_NOERROR then begin
ResultMsg := 'Could not prepare header';
Exit;
end;
CurrentMMResult := WaveOutWrite(FOutputHandle,
FWaveHeaderArray[j], SizeOf(WaveHdr));
if CurrentMMResult<>MMSYSERR_NOERROR then begin
WaveOutStatus := wosError;
WaveOutGetErrorText(CurrentMMResult,
PChar(ResultMsg), MsgLength);
Exit;
end;
end;
WaveOutRestart(FOutputHandle);
Result := True;
end;
end;
function TForm1.ContinuePlaying(WaveHdrPtr: Integer;
var ResultMsg: string): Boolean;
var
j : Integer;
CurrentLength : Cardinal;
TempWavHdr : PWaveHdr;
begin
Result := False;
TempWavHdr := PWaveHdr(WaveHdrPtr);
CurrentLength := 0;
if WaveOutStatus = wosPlaying then
begin
LoadNextDataBlock(
CurrentLength, PWaveHdr(TempWavHdr));
TempWavHdr.dwBufferLength := CurrentLength;
CurrentMMResult := WaveOutPrepareHeader(
FOutputHandle, TempWavHdr, SizeOf(WAVEHdr));
CurrentMMResult := WaveOutWrite(FOutputHandle,
TempWavHdr, SizeOf(WaveHdr));
Result := CurrentMMResult=MMSYSERR_NOERROR;
if not Result then Exit;
end
else
begin
// Iterate through two buffers.
for j := 0 to (MaxBuffers-1) do
if ((FWaveHeaderArray[j].dwFlags and
(1 shl WHdr_InQueue))=(1 shl WHdr_InQueue)) then
begin
Result := True;
Exit;
end;
end;
end;
procedure TForm1.CloseDownPlayback;
var
j: Integer;
begin
if (WaveOutStatus = wosClosed) then Exit;
Label1.Caption := ClosingDownPlaybackMsg;
Label1.Invalidate;
// Always call waveOutReset before waveOutClose.
CurrentMMResult := WaveOutReset(FOutputHandle);
for j := 0 to 1 do
CurrentMMResult :=WaveOutUnprepareHeader(
FOutputHandle, FWaveHeaderArray[j], SizeOf(WAVEHdr));
CurrentMMResult := WaveOutClose(FOutputHandle);
WaveOutStatus := wosClosed;
end;
end;
Sound + Vision
IntToStr(hi(FWaveInCaps.vDriverVersion)) +
'.' + IntToStr(lo(FWaveInCaps.vDriverVersion)));
TreeView1.Items.AddChild(Node, 'Product Name: ' +
FWaveInCaps.szPname);
FormatNode := TreeView1.Items.AddChild(
Node, 'Formats Supported: ');
for k := 0 to (WaveFormatTotal-1) do
if (FWaveInCaps.dwFormats and
(1 shl (k+1))=(1 shl (k+1))) then
TreeView1.Items.AddChild(
FormatNode, WaveFormatArray[k]);
FormatNode := TreeView1.Items.AddChild(
Node, 'Functions Supported: ');
TreeView1.Items.AddChild(Node, 'Channels Supported: ' +
IntToStr(FWaveInCaps.wChannels));
end;
end;
procedure TForm1.btnShowWaveOutPropertiesClick(
Sender: TObject);
var
k: Integer;
j: Integer;
Node, FormatNode: TTreeNode;
begin
TreeView1.Items.Clear;
for j := (FNumOutputDevices-1) downto 0 do begin
waveOutGetDevCaps(j, @FWaveOutCaps,
SizeOf(TWaveOutCaps));
Node := TreeView1.Items.AddChildFirst(
nil, 'Device ' + IntToStr(J+1));
TreeView1.Items.AddChild(Node, 'Manufacturer ID: ' +
IntToStr(FWaveOutCaps.wMid));
TreeView1.Items.AddChild(Node, 'Product ID: ' +
IntToStr(FWaveOutCaps.wPid));
TreeView1.Items.AddChild(Node, 'Driver Version: ' +
IntToStr(hi(FWaveOutCaps.vDriverVersion)) +
'.' + IntToStr(lo(FWaveOutCaps.vDriverVersion)));
TreeView1.Items.AddChild(Node, 'Product Name: ' +
FWaveOutCaps.szPname);
FormatNode := TreeView1.Items.AddChild(
Node, 'Formats Supported: ');
for k := 0 to (WaveFormatTotal-1) do
if (FWaveOutCaps.dwFormats and
(1 shl (k+1))=(1 shl (k+1))) then
TreeView1.Items.AddChild(
FormatNode, WaveFormatArray[k]);
FormatNode := TreeView1.Items.AddChild(
Node, 'Functions Supported: ');
for k := 0 to WaveFunctionTotal-1 do
if (FWaveOutCaps.dwSupport and
(1 shl (k+1))=(1 shl (k+1))) then
TreeView1.Items.AddChild(
FormatNode, WaveFunctionArray[k]);
TreeView1.Items.AddChild(Node, 'Channels Supported: ' +
IntToStr(FWaveOutCaps.wChannels));
end;
end;
Dynamic Delphi
By Shiv Kumar
ISAPI Development
Part II: Using Forms to Dynamically Generate Web Pages
ast month in Part I, you constructed your first ISAPI application with Delphi. You also
learned how to deal with Web browser requests and responses to and from your ISAPI
application. In Part II, youll build a larger ISAPI application by having Web site owners
add a link to their site from your Web site. Instead of manually making these additions in
your database, youll learn how to automate this process by:
Dynamic Delphi
const
NewLine = #13#10;
Instead of NewLine, you may prefer crlf because its easier to type.
Also, remember that you dont need to have a new line (you only
do this to format the generated HTML); the browser will render
the page in exactly the same way, with or without the new line.
Take a closer look at the forms actions:
'<form action="' + Request.ScriptName +
'/PostLinkInfo" method="post"' + NewLine +
' title="Enter the information required ' +
'to Add a link to your Web Site.">'
The Request objects ScriptName property returns the name and path
of the ISAPI DLL. In this case, its /scripts/AddLink.dll. Therefore, the value of the action attribute of the form is:
/scripts/AddLink.dll/PostLinkInfo
This action tells the browser what to do when the form is submitted by the Submit button. It becomes obvious then that you need
a new action in your project with the PathInfo property set to
/PostLinkInfo. In other words, when this form is submitted, the
Web Brokers dispatcher will look for an action whose PathInfo
property is /PostLinkInfo, and re its OnAction event. This
is where you write code to validate and post the information
received.
The Method attribute generally can be either POST or GET. (The
difference between the two will be explored later.) The Title attribute
produces the hint or tool tip for the form in the browser.
For now, return to the OnAction event shown in Listing One.
Notice youre sending back (by using the Response objects Content
property) the HTML required for a form. The Content property is a
string that is sent back via the Web server to the requesting browser.
For this example, visitors (who are interested in adding a link to
their site from your Web site) need to ll in the following elds:
The URL to their site.
The links title to appear on your Web site.
A short description of their Web site.
The Web site owners rst name.
The Web site owners last name.
The Web site owners e-mail address.
By looking at the form in Figure 2, youll notice that the URL eld
already has the text http://. This is done when the form is generated,
by entering http:// in the value attribute as follows:
'<td><font face="Tahoma"><input type="text" ' +
'name="txtLinkURL"' + NewLine + ' style="height: ' +
'22px; width: 274px" value="http://"></font></td>' +
'</tr>' + NewLine +
However, before you can use this URL, your Web server and HTTP
service must be started. You also need to compile your application
and make sure that AddLink.dll exists in your Web servers scripts
folder. Remember to replace ServerName with your PCs name, IP
address (127.0.0.1), or domain name. After youve completed these
tasks, you can type in the URL and see the form (see Figure 2).
Tip: If you set the Default property of AddLinkPage to True, you
can simply type in http://ServerName/scripts/AddLink.dll and
get the same form. (There can be only one default action in an
ISAPI/CGI project.)
Notice also that each eld on the form (recognized by the <input>
tag) has a unique name such as txtLinkURL. Later, youll use these
names to extract the values for each of the required elds in order to
post this information to a database.
So far, you have an action item in your ISAPI DLL that will generate
a form. To call this action, you would enter the following URL in
your Web browsers address eld:
Now, add Data Access components and set a few properties. Drop
Database, Query, and Session components on the Web Module. Set the
AliasName property of the database component to WEBSITE_ODBC,
Dynamic Delphi
the DatabaseName property of the database component to
DELPHILINKS, the DatabaseName of Query1 to DELPHILINKS, and
the Login property of the Database component to False. Finally, set the
Params property of the Database component to:
username=admin
password=
AddLink.DLL.
Also note that the Request and Response objects are actually public
properties of the Web Module, which means theyre accessible
throughout the module even in your own methods.
The data of the form is available in the ContentFields property
of the Request object because the POST method has been used
(method=post) Request because it comes to this event as a request
made by the browser via the Web server. If you use the GET method
in the form, then the same information would be made available to
you in the QueryFields property of the Request object.
To make data validation easier, rst create two custom Exception
objects in the type section that can be raised if any part of the data
is invalid:
type
EInsufficientInfo = class(Exception);
EInvalidURL = class(Exception);
...
The ContentFields and QueryFields properties are TStrings descendants. The forms data is made available to you in the form of
name=value pairs, so the data entered in the URL eld looks like
txtLinkURL=http://www.matlus.com. Therefore, you can use the
Values method of the ContentFields object to get the value for a given
eld name. The code in this action items OnAction event can be seen
in Listing Two beginning on page 23.
Notice that the error information is sent back to the browser
in the form of HTML, so the user is informed of the error(s).
This also comes in handy while debugging, because youll see
all errors in the browser even a key-violation error, or some
other database-related error. As mentioned earlier, the database
programming aspect is like that of any other Delphi database
application.
Now, look at the following statement:
Response.SendRedirect(
Request.ScriptName + '/ShowDelphiLinks');
If the data was posted successfully to the table, youll want to show
a list of links in your database. This line will redirect the browser to
the action in your ISAPI DLL to make that happen. You have yet to
create that action and the code for it, but youll do that soon.
The code for the URLIsValid function is:
function TWebMod1.URLIsValid(URL: string): Boolean;
begin
try
// If URL isn't found, an exception will be raised.
IdHTTP1.Get(URL);
Result := True;
except
Result := False;
end;
end;
Dynamic Delphi
Recompiling Your Project
If you need to recompile your project for any reason as an ISAPI DLL,
you must stop and restart the WWW service, and then recompile
your application. (This isnt necessary for a CGI application). If youre
using Internet Information Server (IIS), you can uncheck the Cache
ISAPI Applications checkbox in the Internet Service Manager options.
Be sure to turn it on in your production Web server machine. IIS has
the ISAPI DLL in use, and therefore wont allow you to overwrite the
file until it is unloaded from memory. This is accomplished by either
shutting down the service (closing the program), or selecting an option
in the Web server to unload the DLL after each use.
To release the lock on a DLL, Personal Web Server (PWS) must be
manually stopped using the command prompt. By default, Pws.exe
resides in the folder C:\Windows\System\Inetsrv. To stop PWS, type
the following command:
Windows\System\inetsrv\pws.exe /stop
You can then restart PWS manually from the command prompt, thus:
Windows\System\inetsrv\pws.exe /start
has been used in this article. If you use the GET method, the URL
changes when you hit the Submit button. The browser will add to the
URL path the information as parameters to send back to the server.
The URL would look like:
Once PWS is stopped, this will release the ISAPI DLL. Recompile the
DLL, if necessary. Once PWS restarts, it will again lock the DLL when
its instantiated by your browser.
http://ServerName/scripts/AddLink.dll/
PostLinkInfo?txtLinkURL=http://mywebsite.com&txtTitle=
My+Site+Title&txtDescription=My&Sites&description
For IIS 4.0. At the run line, type net stop iisadmin /y, and then
press J. This will stop all services while recompiling your DLL.
Your application accesses the data from the Request objects QueryFields
property, rather than its ContentFields property. It extracts the values
from Request.QueryFields in the same manner as Request.ContentFields,
because both properties are TStrings descendants.
The advantage of the GET method is that it doesnt require a form
to send information back to your application. You can simply append
the value you need to send to the end of the URL. In the POST
method, the information is captured from the form and sent as a
separate stream. Of course, you may nd that the POST method is
advantageous because the information doesnt appear in the address
box of the browser. Additionally, the POST method can handle much
greater volumes of information. The GET method has a limit to the
amount of data that can be sent in this way. Also, the URL needs to
be encoded. The browser does this automatically for you when the
GET method is used in a form.
Tip: The HTTPApp.pas unit contains two functions, named
HTTPEncode and HTTPDecode, that will come in handy for future
ISAPI/CGI work.
After recompiling your DLL, type net start w3svc from the run
line. Your services will be up again. This application uses the Services
applet to stop/start the WWW service.
For IIS 5.0.The command line is simply iisreset, or you can use the
Service administrative tool to stop-start the WWW service.
Shiv Kumar
Now, enter the code in Listing Three (on page 24), in the OnAction
event. Notice this fragment in Listing Three:
'<img src="/Images/DelphiLinksHead.gif">' + NewLine +
'<a href="' + Request.ScriptName + '/AddLinkPage">' +
'<img alt="Add a link to your Web site from here" ' +
'border="0" src="' + '/Images/AddLink.gif"></a>' +
NewLine + '<p/><hr/>' + NewLine + '<table>' + NewLine;
Two images are referenced here with the <img> tags. These images
should be in the Images subfolder of your Web servers root folder.
You can call this subfolder anything you like, but be sure to
change the code to match your subfolders name. [Note: Its good
practice to store your images in a separate folder, rather than in
the root folder. They are easier to manage this way.]
The second image in the previous code is hyperlinked. This link
will direct users to the data entry screen. Also notice the line
with the Format procedure. This line of code results in a string
like this:
Dynamic Delphi
<a href="http://www.matlus.com">
<b>The Delphi Apostle</b></a>
Conclusion
There you have it; an ISAPI application where users can enter data
into a form, send the information back to your database, and have
it appear on your Web site in a dynamically generated HTML page.
In the next part of this series, youll learn how to extract images from
a database table, shrink images (thumbnails) on the fly to reduce the
required bandwidth, and increase the speed of your Web site.
The files referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103SK.
Shiv Kumar works in R&D at DataSource Inc. He develops new product ideas,
prototypes them, and leads the implementation effort. Shiv, an ex-VB programmer,
has used Delphi exclusively since Delphi 1. His hobbies include electronics and
photography, and he loves to ride his CBR 1100XX at every opportunity. You can
contact Shiv at shiv@matlus.com.
procedure TWebMod1.WebMod1waPostLinkInfoAction(
Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
{ A custom Exception object called EInsufficientInfo will
Dynamic Delphi
be raised if any part of the data is invalid. In this
case, invalid = null. }
try
{ Validate data entered by the user. }
if Request.ContentFields.Values['txtLinkURL'] = '' then
raise EInsufficientInfo.Create(
'Please Enter the URL of your Web Site');
if Request.ContentFields.Values['txtTitle'] = '' then
raise EInsufficientInfo.Create('Please Enter the' +
' Title you want Displayed for your Web Site');
if Request.ContentFields.Values[
'txtDescription'] = '' then
raise EInsufficientInfo.Create('Please Enter the' +
' Description you want Displayed for your Web Site');
if Request.ContentFields.Values[
'txtOwnerFirstName'] = '' then
raise EInsufficientInfo.Create(
'Please Enter Your First Name.');
if Request.ContentFields.Values[
'txtOwnerLastName'] = '' then
raise EInsufficientInfo.Create(
'Please Enter Your Last Name');
if Request.ContentFields.Values[
'txtEmailAddress'] = '' then
raise EInsufficientInfo.Create(
'Please Enter your contact Email Address');
{ Validate the URL that the user has entered as well.
To do that, use an HTTP component in the URLIsValid
function. Warn the user if the URL is invalid. }
if not URLIsValid(Request.ContentFields.Values[
'txtLinkURL']) then
raise EInvalidURL.Create('The URL you submitted is' +
' not a valid URL.' + #13#10 + 'Please make sure ' +
'you type a valid URL before submitting');
{ If data and URL are valid, post it to the database. }
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('INSERT INTO LINKS');
Query1.SQL.Add('(LinkURL, Title, Description,
DateAdded, OwnerFirstName, OwnerLastName,
EmailAddress)');
Query1.SQL.Add('Values(:LinkURL, :Title, :Description,
:DateAdded, :OwnerFirstName, :OwnerLastName,
:EmailAddress)');
Query1.ParamByName('LinkURL').AsString :=
Request.ContentFields.Values['txtLinkURL'];
Query1.ParamByName('Title').AsString :=
Request.ContentFields.Values['txtTitle'];
Query1.ParamByName('Description').AsString :=
Request.ContentFields.Values['txtDescription'];
Query1.ParamByName('DateAdded').AsDateTime := Now;
Query1.ParamByName('OwnerFirstName').AsString :=
Request.ContentFields.Values['txtOwnerFirstName'];
Query1.ParamByName('OwnerLastName').AsString :=
Request.ContentFields.Values['txtOwnerLastName'];
Query1.ParamByName('EmailAddress').AsString :=
Request.ContentFields.Values['txtEmailAddress'];
Query1.ExecSQL;
{ If everything is fine, show the user the Links page,
so he can see the link he just added. }
Response.SendRedirect(
Request.ScriptName + '/ShowDelphiLinks');
except
on E : Exception do
Response.Content := Format('<html><head><title>' +
'Error Posting Inforamtion</title>' + '</head>' +
'<body><H1>%s</H1></body></html>', [E.Message]);
end;
end;
Greater Delphi
By Robert Leahey
Better UI Design
Part II: Building a Message Queue Class
ast month, in this space, we looked at some of the common problems in bad
user interface (UI) design. We indicated that, while the primary goal of good user
interaction design is to make the users more productive, there are common UI problems
that hamper our users ability to be productive.
This month, we examine a possible solution to
one of the more ubiquitous of bad UI design problems, that of excessive message dialog boxes. The
constant barrage of needless confirmation dialog
boxes, status messages, and unhandled exceptions
is a major source of disruption for users.
Do you have things to tell your users? Sure. Do you
have things to ask them? Of course. But by popping up a message every time we have something
to say or ask, we are interrupting the users and
forcing them to deal with our priorities instead of
theirs. Thats rude. Would you want to work with
someone capable of this exchange?
Your files were checked in successfully. OK?
The file was renamed. OK?
The user cancelled the action. OK?
You really screwed up this time and your hard disk
will be reformatted when you click OK. OK?
Figure 1: The
bane of our
existence.
Greater Delphi
unit buiMessageQueue;
interface
uses
SysUtils, Windows, Classes, buiParenting, buiTypes,
buiMessages, buiMessageListInterface;
type
TbuiMessageQueue = class (TbuiParenting)
private
FOnOutOfSynch: TNotifyEvent;
FOutOfSynch: Boolean;
FRemoveWhenRead: Boolean;
function GetContinueProcess: Boolean;
function GetHasUnreadMessages: Boolean;
function GetHighestUnreadPriority: TbuiMessagePriority;
function GetMessageCount: Integer;
function GetMessages(aIndex: Integer): TbuiMessage;
function GetUnreadMessageCount: Integer;
function MessageOfTypeExists(
aMessageClass: TbuiMessageClass): Boolean;
procedure SetOutOfSynch(aOutOfSynch: Boolean);
protected
function AllowNewMessageOfType(
aMessageClass: TbuiMessageClass): Boolean; virtual;
procedure DoOnOutOfSynch;
public
constructor Create;
class function ParentedClass: Boolean; override;
procedure AddMessage(aMessageClass: TbuiMessageClass;
const aCaption, aText, aCategory: string;
aAllowProcess: Boolean;
aPriority: TbuiMessagePriority;
aReadEvent: TbuiReadMessageEvent); virtual;
procedure MessageRead(aIndex: Integer); virtual;
procedure PopulateMessageList(
aMessageList: IbuiMessageList); virtual;
procedure RemoveReadMessages; virtual;
property ContinueProcess: Boolean
read GetContinueProcess;
property HasUnreadMessages: Boolean
read GetHasUnreadMessages;
property HighestUnreadPriority: TbuiMessagePriority
read GetHighestUnreadPriority;
property MessageCount: Integer read GetMessageCount;
property Messages[aIndex: Integer]: TbuiMessage
read GetMessages; default;
property OnOutOfSynch: TNotifyEvent
read FOnOutOfSynch write FOnOutOfSynch;
property OutOfSynch: Boolean read FOutOfSynch;
property RemoveWhenRead: Boolean
read FRemoveWhenRead write FRemoveWhenRead;
property UnreadMessageCount: Integer
read GetUnreadMessageCount;
end;
Classes Overview
The following classes make up this messaging queue system. Note:
For the code presented in this series of articles, Ive adopted the prex
of bui for Better UI.
TbuiParenting (declared in buiParenting.pas) is the ancestor for
the queue and message classes. It introduces behaviors specic to
parent/child relationships.
TbuiMessageQueue (declared in buiMessageQueue.pas) is the
queue class. It manages a list of messages, allowing the programmer to add and retrieve them for users.
TbuiMessage (declared in buiMessages.pas) is a class representing a message that is sent to the queue. Instances of this class
26 March 2001 Delphi Informant Magazine
constructor TbuiMessageQueue.Create;
begin
inherited Create;
FOutOfSynch := True;
FRemoveWhenRead := True;
end;
class function TbuiMessageQueue.ParentedClass: Boolean;
begin
Result := False;
end;
TbuiMessageQueue Class
Central to our discussion is the class that manages the messages
we want to send to users TbuiMessageQueue. The code for the
interface section of the buiMessageQueue unit is shown in Figure 3.
First, lets look at the public section of TbuiMessageQueue. The
constructor is simple, initializing two elds. ParentedClass is a class
function introduced in TbuiParenting and overridden here to return
a value of False, indicating that this class doesnt have a parent. The
implementations of these two methods are shown in Figure 4.
The next four methods are involved in managing the messages.
AddMessage is called when we need to send a message to the
queue. It has several parameters, but theyre fairly straightforward:
aMessageClass is the type of message were sending. Well discuss
TbuiMessage momentarily, but for now its enough to say that when
we call AddMessage, we pass in the class type of the TbuiMessage
descendant we want the message queue to create. The aCaption,
aText, and aCategory parameters are strings that make up the
message. aAllowProcess is a Boolean value that allows us to
specify whether we want the application to continue accepting
input from the users until they have read the message. If
the queue contains a message for which aAllowProcess is False
and unread, TbuiMessageQueue.ContinueProcess will be False. The
aPriority parameter allows you to set the relative importance of your
message in ve degrees dened by the TbuiMessagePriority type.
Finally, the aReadEvent parameter allows you to specify an event
handler that will be called when the user reads this message.
You can call MessageRead when your users have read a message
from an implementation of IbuiMessageList. Calling MessageRead
will cause the queue object to re the event handler you specied
in AddMessage. The aIndex parameter is the index of the message
thats been read. This value will be provided when the queue object
populates your IbuiMessageList implementation.
The PopulateMessageList method is called to retrieve the messages that
are stored in the queue. You can pass in any implementation of the
IbuiMessageList interface, and the queue object will call its methods
to ll it with the messages.
Greater Delphi
Finally, you can call RemoveReadMessages at any time to force the
queue object to delete any messages that have been read from its list.
To see how these four methods are implemented, see Listing One
(beginning on page 30).
Next, well look at the TbuiMessageQueue class properties (again, see
Figure 3). ContinueProcess is a Boolean property that indicates if any
of the messages owned by the queue need to be read before the
users can continue. By reading this property, you can indicate to your
users visually when theres a message that requires their immediate
attention.
Note that, while it may seem that this is no different than stopping users progress with a pop-up message dialog box, there is,
in fact, a difference. The message dialog box still grabs users
attention with a jarring pop, but with the message queue method
we can provide our users with a much more subtle indicator that
we need their attention. By using a method such as a ashing icon
on the status bar, or by turning the border of their workspace a
different color, we allow the users to discover the message and
take the initiative to read it themselves. This is admittedly a
slight difference, but its the type of difference that can make an
application more enjoyable to use.
HasUnreadMessages indicates whether there are unread messages in
the queue. HighestUnreadPriority polls the queued messages for the
message with the highest priority that has not been read. You can use
this information to alter your applications appearance in some way to
indicate to users the relative importance of the messages waiting for
them. MessageCount is a count of the messages currently stored in the
queue both read and unread.
The indexed Messages property allows you access to the queued messages. OnOutOfSynch is an event that is red anytime the message
function TbuiMessageQueue.AllowNewMessageOfType(
aMessageClass: TbuiMessageClass): Boolean;
begin
Result := True;
if not (aMessageClass.AllowDuplicates) and
MessageOfTypeExists(aMessageClass) then
Result := False;
end;
procedure TbuiMessageQueue.DoOnOutOfSynch;
begin
if Assigned(FOnOutOfSynch) then
FOnOutOfSynch(Self);
end;
function TbuiMessageQueue.MessageOfTypeExists(
aMessageClass: TbuiMessageClass): Boolean;
var
liIndex: Integer;
begin
Result := False;
liIndex := 0;
while not(Result) and (liIndex < MessageCount) do begin
if (Messages[liIndex].ClassType = aMessageClass) then
Result := True
else
Inc(liIndex);
end;
end;
queue changes in such a way that it would be out of sync with any
populated IbuiMessageList implementations. This allows you to call
PopulateMessageList again when the queue changes. OutOfSynch is
the Boolean representation of the synched state of the queue and a
populated list. Set the RemoveWhenRead property to indicate whether
the queue object should delete any messages that have been read.
Finally, the UnreadMessageCount property returns a count of the
unread messages in the queue. Listing Two (on page 31) shows
the implementation of the read-and-write access methods of these
properties.
There are three other methods of TbuiMessageQueue that we
need to examine: MessageOfTypeExists in the private section,
and AllowNewMessageOfType and DoOnOutOfSynch in the
protected section.
MessageOfTypeExists simply takes a parameter of a TbuiMessageClass
type and determines whether a message of this class exists in its list.
This method is used by AllowNewMessageOfType, which determines
whether a new message of a given TbuiMessageClass can be added.
If the given TbuiMessages class method, AllowDuplicates, returns
False and there is already a message of that class type in the queue,
no more will be added.
Finally, DoOnOutOfSynch is a dispatch method to re the
OnOutOfSynch event. Figure 5 shows the implementation of
these methods.
unit buiMessages;
interface
uses
SysUtils, Windows, buiParenting, buiTypes;
type
TbuiReadMessageEvent = procedure(Sender: TObject;
const aMessageText: string;
var aRead: Boolean) of object;
TbuiMessage = class(TbuiParenting)
private
FCaption: string;
FCategory: string;
FContinueProcess: Boolean;
FMessageText: string;
FOnReadMessage: TbuiReadMessageEvent;
FPriority: TbuiMessagePriority;
FRead: Boolean;
public
constructor Create;
class function AllowDuplicates: Boolean; virtual;
procedure DoReadMessage(aSender: TObject);
property Caption: string read FCaption write FCaption;
property Category: string
read FCategory write FCategory;
property ContinueProcess: Boolean
read FContinueProcess write FContinueProcess;
property MessageText: string
read FMessageText write FMessageText;
property OnReadMessage: TbuiReadMessageEvent
read FOnReadMessage write FOnReadMessage;
property Priority: TbuiMessagePriority
read FPriority write FPriority;
property Read: Boolean read FRead write FRead;
end;
TbuiMessageClass = class of TbuiMessage;
Greater Delphi
TbuiParenting Class
As ancestor to TbuiMessageQueue and TbuiMessage, TbuiParenting
provides some basic functionality for managing parent/child relationships. Listing Three (beginning on page 31) shows the entire
buiParenting.pas unit.
constructor TbuiMessage.Create;
begin
inherited Create;
FRead := False;
FPriority := mpNormal;
end;
Again, well examine the public section of TbuiMessage rst. The constructor initializes two elds while the class method AllowDuplicates is
introduced. AllowDuplicates allows you to create a message class that
allows only one instance within the queue object. To do so, you would
create a descendant that would override AllowDuplicates to return False.
Figure 7 shows the implementation of these two methods.
DoReadMessage is the dispatch method for the OnReadMessage event.
Figure 8 shows its implementation.
Next, well take a look at the properties of TbuiMessage.
Caption is a string that describes the message to the users.
When TbuiMessageQueue.PopulateMessageList is called, it passes the
Caption property to the IbuiMessageList implementation. In the
case of TbuiTreeView, which implements IbuiMessageList, Caption is
used as the text for the tree nodes.
The Category property is similar in usage to the Caption property,
but it can be used by IbuiMessageList implementations to categorize messages. With TbuiTreeView, we create parent nodes by
Category and add message nodes beneath them. ContinueProcess
is a Boolean property that indicates whether we want our application to allow processing until this message has been read. This
property is accessed by TbuiMessageQueue.GetContinueProcess as
discussed before.
The MessageText string property is the actual text of the message.
OnReadMessage is an event that will re when the users read this
message. Calling TbuiMessageQueue.MessageRead will cause the specied event handler to re. Priority is of type TbuiMessagePriority as
dened in buiTypes.pas. Heres the declaration:
TbuiMessagePriority = (mpLowest, mpLow, mpNormal,
mpHigh, mpHighest);
And lastly, Read. This Boolean indicates whether this message has
been read by the users.
IbuiMessageList Interface
The IbuiMessageList interface is provided as a way for
TbuiMessageQueue to populate some sort of list with the messages
it contains. (Note: Delphis interfaces are outside the scope of
this article. Search Delphi online help for Object Interfaces to
learn more.) Sufce it to say that any object that implements a
dened interface can be used where that interface is expected. For
our purposes, we have dened an interface that provides a way
for TbuiMessageQueue to add messages to a list. Its up to the
implementing class to dene what should be done with the messages it receives. Figure 9 shows the buiMessageListInterface unit.
TbuiMessageQueue.PopulateMessageList (see Figure 10) is the method
that accepts IbuiMessageList implementations.
TbuiTreeView Class
The TbuiTreeView component is provided as an example implementation of the IbuiMessageList interface. Obviously, you can create
your own implementations, but this component serves as an excellent
example of how an implementation might be realized. The code for
TbuiTreeView from the buiControls unit can be seen in Listing Four
(beginning on page 32).
TbuiTreeView is fairly self evident, so well forego direct examination
of the component in favor of seeing it in action.
Greater Delphi
Implementing an event handler for the message queue objects
OnOutOfSynch event (which will re every time we add
or remove a message from the queue), we call
TbuiMessageQueue.PopulateMessageList for our IbuiMessageList
implementing controls. This way, every time a message is added to
the queue, the lists are updated. In addition, we check the queues
HighestUnreadPriority property so we can alter the appearance of the
application as higher priority messages arrive in the queue.
If you look at Figure 11 again, youll notice a red border around the
text workspace and a red ag on a button in the lower-left corner of
the window. These visual cues indicate to the user that a very high
priority message is in the queue an access violation in this case.
Because of the severity of this message, I wanted to limit the users
ability to proceed until he or she had read this message. To that
end, most of the event handlers in this application check the value
of the queue objects ContinueProcess property before executing their
code. With an access violation unread in the queue, the value would
be False, and the application would visually indicate that the users
should check the messages by ashing the red border.
In the tree views OnChange event, we call the queue objects MessageRead
method. Part of the declaration of the IbuiMessageLists AddMessage
method is the aMessageIndex parameter. By storing this index in our tree
unit buiMessageListInterface;
interface
uses
SysUtils, Windows, buiTypes;
type
IbuiMessageList = interface (IUnknown)
['{ 1B464763-A224-11D4-9EC9-F04C51C10100 }']
procedure AddMessage(const aCategory, aCaption,
aMessageText: string; aPriority: TbuiMessagePriority;
aRead: Boolean; aMessageIndex: Integer); stdcall;
procedure ClearList; stdcall;
end;
nodes Data property, we can pass that value back to the queue in the
MessageRead method. Obviously, this is a supercial look at the use of
these classes, so its recommended that you download the demonstration
and examine it more closely.
implementation
end.
procedure TbuiMessageQueue.PopulateMessageList(
aMessageList: IbuiMessageList);
var
liIndex: Integer;
lMessage: TbuiMessage;
begin
if (aMessageList <> nil) then begin
RemoveReadMessages;
aMessageList.ClearList;
for liIndex := 0 to MessageCount - 1 do begin
lMessage := Messages[liIndex];
aMessageList.AddMessage(lMessage.Category,
lMessage.Caption, lMessage.MessageText,
lMessage.Priority, lMessage.Read, liIndex);
end;
SetOutOfSynch(False);
end;
end;
Greater Delphi
box. Can you nd it? Were the ones whore supposed to be good
at nding the solutions to tough problems, so do your users a favor
by making them more productive. Find a better option than the
ubiquitous message dialog box.
Next month: If its worth asking, its worth the program remembering; in other words, better user option persistence.
The les referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103RL.
Greater Delphi
lMessage.Caption, lMessage.MessageText,
lMessage.Priority, lMessage.Read, liIndex);
end;
SetOutOfSynch(False);
end;
end;
procedure TbuiMessageQueue.RemoveReadMessages;
var
liIndex: Integer;
lMessage: TbuiMessage;
begin
for liIndex := MessageCount - 1 downto 0 do begin
lMessage := Messages[liIndex];
if (lMessage.Read) then
lMessage.Free;
end;
end;
var
liIndex: Integer;
begin
Result := 0;
for liIndex := 0 to MessageCount - 1 do
if not (Messages[liIndex].Read) then
Inc(Result);
end;
procedure TbuiMessageQueue.SetOutOfSynch(
aOutOfSynch: Boolean);
begin
FOutOfSynch := aOutOfSynch;
if (FOutOfSynch) then
DoOnOutOfSynch;
end;
Greater Delphi
procedure TbuiParenting.FreeChildren;
var
liIndex: Integer;
begin
FFreeingChildren := True;
try
for liIndex := (FChildren.Count - 1) downto 0 do
TObject(FChildren[liIndex]).Free;
finally
FChildren.Clear;
FFreeingChildren := False;
end;
end;
function TbuiParenting.GetChildCount: Integer;
begin
Result := FChildren.Count;
end;
function TbuiParenting.GetChildren(aIndex: Integer):
TbuiParenting;
begin
Result := TbuiParenting(FChildren[aIndex]);
end;
class function TbuiParenting.ParentedClass: Boolean;
begin
Result := True;
end;
function TbuiParenting.RemoveChild(aChild: TbuiParenting):
Integer;
begin
Result := FChildren.Remove(aChild);
end;
procedure TbuiParenting.SetParent(aValue: TbuiParenting);
begin
if (ParentedClass) and (FParent <> aValue) then begin
if (FParent <> nil) then FParent.RemoveChild(Self);
FParent := aValue;
if (FParent <> nil) then FParent.AddChild(Self);
end;
end;
end.
aPriority: TbuiMessagePriority);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddMessageText(aMessageIndex: Integer;
const aMessageText: string);
function FindNodeByCaption(const aCaption: string):
TTreeNode;
procedure ClearList; stdcall;
procedure AddMessage(const aCategory, aCaption,
aMessageText: string; aPriority: TbuiMessagePriority;
aRead: Boolean; aMessageIndex: Integer); stdcall;
published
property OnAddCategoryNode: TbuiAddCategoryNodeEvent
read FOnAddCategoryNode write FOnAddCategoryNode;
property OnAddMessageNode: TbuiAddMessageNodeEvent
read FOnAddMessageNode write FOnAddMessageNode;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Better UI', [TbuiTreeView]);
end;
constructor TbuiTreeView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FMessageTexts := TStringList.Create;
end;
estructor TbuiTreeView.Destroy;
begin
FMessageTexts.Free;
inherited Destroy;
end;
procedure TbuiTreeView.AddMessage(const aCategory,
aCaption, aMessageText: string;
aPriority: TbuiMessagePriority; aRead: Boolean;
aMessageIndex: Integer);
var
lNode: TTreeNode;
lMessageNode: TTreeNode;
lsCategory: string;
begin
if not(aRead) then
lsCategory := aCategory
else
lsCategory := 'Read: ' + aCategory;
lNode := FindNodeByCaption(lsCategory);
if (lNode = nil) then begin
if (aRead) then
lNode := Items.AddChildObject(
nil, lsCategory, Pointer(-1))
else
lNode := Items.AddChildObjectFirst(
nil, lsCategory, Pointer(-1));
DoOnAddCategoryNode(Self, lNode);
end;
lMessageNode := Items.AddChildObject(
lNode, aCaption, Pointer(aMessageIndex));
DoOnAddMessageNode(Self, lMessageNode, aRead, aPriority);
AddMessageText(aMessageIndex, aMessageText);
end;
procedure TbuiTreeView.AddMessageText(
aMessageIndex: Integer; const aMessageText: string);
begin
while (FMessageTexts.Count <= aMessageIndex) do
FMessageTexts.Add('');
FMessageTexts[aMessageIndex] := aMessageText;
end;
procedure TbuiTreeView.ClearList;
begin
Greater Delphi
Items.Clear;
FMessageTexts.Clear;
end;
procedure TbuiTreeView.DoOnAddCategoryNode(Sender: TObject;
aNode: TTreeNode);
begin
if Assigned(FOnAddCategoryNode) then
FOnAddCategoryNode(Sender, aNode);
end;
procedure TbuiTreeView.DoOnAddMessageNode(Sender: TObject;
aNode: TTreeNode; aRead: Boolean;
aPriority: TbuiMessagePriority);
begin
if Assigned(FOnAddMessageNode) then
FOnAddMessageNode(Sender, aNode, aRead, aPriority);
end;
function TbuiTreeView.FindNodeByCaption(
const aCaption: string): TTreeNode;
var
lNode: TTreeNode;
begin
Result := nil;
lNode := Items.GetFirstNode;
while (lNode <> nil) and (lNode.Text <> aCaption) do
lNode := lNode.GetNext;
if (lNode <> nil) then
Result := lNode;
end;
end.
At Your Fingertips
By Bruno Sonnino
ometimes you need information about your systems installed drives and files, for
example, which drives are installed. This article shows you how to retrieve this
information and place it in a ListBox. It also demonstrates how to retrieve and use the
system image list, which is used to cache the icons used in Windows.
Description
DRIVE_REMOVABLE
DRIVE_FIXED
DRIVE_REMOTE
DRIVE_CDROM
DRIVE_RAMDISK
At Your Fingertips
var
CurrDrive : string;
begin
// Get executable drive.
CurrDrive := ExtractFileDrive(ParamStr(0));
// Check if executable drive is CD-ROM.
if GetDriveType(PChar(CurrDrive+'\')) <> DRIVE_CDROM then
MessageDlg('This program isn't run from a CD-ROM',
mtError, [mbOk], 0);
Figure 4: A
systems installed
drives.
Attribute
Description
SHGFI_ATTRIBUTES
SHGFI_DISPLAYNAME
SHGFI_EXETYPE
SHGFI_ICON
SHGFI_ICONLOCATION
SHGFI_LARGEICON
SHGFI_LINKOVERLAY
SHGFI_OPENICON
SHGFI_PIDL
SHGFI_SELECTED
SHGFI_SHELLICONSIZE
SHGFI_SMALLICON
SHGFI_SYSICONINDEX
Retrieves the index of the icon within the system image list, in iIcon.
SHGFI_TYPENAME
SHGFI_USEFILEATTRIBUTES
to SizeOf(TSHFileInfo). The last parameter, uFlags, is a ag combination that determines the information to be retrieved.
The TSHFileInfo structure will hold the information requested about
the le, and the values it will contain depend on the uFlags parameter.
This structure is dened like this:
_SHFILEINFOA = record
hIcon: HICON;
// File icon handle.
iIcon: Integer;
// File icon index.
dwAttributes: DWORD; // File attributes.
// Display name.
szDisplayName: array [0..MAX_PATH-1] of AnsiChar;
szTypeName: array [0..79] of AnsiChar; // File type.
end;
TSHFileInfo = TSHFileInfoA;
The uFlags parameter must contain a Boolean value, or some combination of the ags shown in Figure 5. The members of the
TSHFileInfo structure will be lled with valid data, depending on
the uFlags combination.
For example, if we want to retrieve the normal icon and display name
for a le, we can use a call like this:
SHGetFileInfo(PChar(Filename), 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_DISPLAYNAME or SHGFI_ICON);
The last parameter tells the function to retrieve the display name,
copying it to szDisplayName; and the les icon, putting its handle
in the hIcon member of the return structure. The code shown in
Figure 6 retrieves the large and small icons, displaying them in two
Image components on the form. Figure 7 shows a sample result.
Notice that SHGetFileInfo is called twice, once for each icon. To
check if the icon is valid, test the return code from SHGetFileInfo.
35 March 2001 Delphi Informant Magazine
When we request a le icon, the return code should be an ImageList handle (more on this in the next tip), or 0, if the le is
not valid.
At Your Fingertips
procedure TForm1.Button1Click(Sender: TObject);
var
ShFileInfo : TSHFileInfo;
SysListHandle : THandle;
begin
// Retrieve small icon, type name, and icon location
// in the system image list.
SysListHandle := SHGetFileInfo(PChar(Edit1.Text), 0,
ShFileInfo, SizeOf(TSHFileInfo), SHGFI_DISPLAYNAME or
SHGFI_SMALLICON or SHGFI_ICON or SHGFI_TYPENAME or
SHGFI_ICONLOCATION);
// Assign retrieved icon to the small icon Image.
Image1.Picture.Icon.Handle := ShFileInfo.hIcon;
// If SysListHandle is <> 0, it's a valid file.
if SysListHandle <> 0 then
begin
// Assign display name label.
Label3.Caption := string(ShFileInfo.szDisplayName);
// Assign icon index label.
Label5.Caption := IntToStr(ShFileInfo.iIcon);
// Assign type name label.
Label7.Caption := string(ShFileInfo.szTypeName);
// 2nd call to SHGetFileInfo to retrieve normal icon.
SHGetFileInfo(PChar(Edit1.Text), 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_ICON);
// Assign retrieved icon to large icon TImage.
Image2.Picture.Icon.Handle := ShFileInfo.hIcon;
end
else
begin
// Handle invalid filename.
Label3.Caption := 'Not a valid filename';
Label5.Caption := '';
Label7.Caption := '';
end;
end;
Drawing in a ListBox
Now lets use all of the ideas discussed so far this month. One
thing, however, that wasnt mentioned regarding the SHGetFileInfo
function, is that you can also use it to retrieve drive icons. Well use
this capability to place the icon and display name in a ListBox.
We wont store the icons ourselves. Instead, we will use an ImageList that will share images with the system image list of small
At Your Fingertips
procedure TForm1.FormCreate(Sender: TObject);
const
DriveTypes : array[0..4] of string = ('Removable',
'Fixed Disk', 'Network Drive', 'CD-ROM','Ram Disk');
var
Drive : Char;
ShFileInfo : TSHFileInfo;
DriveType : Integer;
begin
// Get icon width and height.
FIconWidth := GetSystemMetrics(SM_CXSMICON);
Listbox1.ItemHeight := GetSystemMetrics(SM_CXSMICON) + 4;
// Assign system image list to ImageList component.
ImageList1.Handle := SHGetFileInfo('', 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_SYSICONINDEX or
SHGFI_SMALLICON or SHGFI_ICON);
for Drive := 'A' to 'Z' do begin
// Get display name and icon index for drive.
SHGetFileInfo(PChar(Drive+':\'), 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_SYSICONINDEX or
SHGFI_DISPLAYNAME);
// Get drive type. If it's valid, add drive and
// icon index to the ListBox.
DriveType := GetDriveType(PChar(Drive+':\'));
if DriveType in [DRIVE_REMOVABLE..DRIVE_RAMDISK] then
Listbox1.Items.AddObject(
string(ShFileInfo.szDisplayName) + '
' +
DriveTypes[DriveType-DRIVE_REMOVABLE],
TObject(ShFileInfo.iIcon));
end;
end;
Figure 13:
Sample ListBox with
installed
drives and
icons.
Conclusion
Getting information about installed drives and disk les is easy, using a
couple of Windows API functions. As a bonus, with the SHGetFileInfo
function, you can also access the system image list and use its icons, in
much the same way you use an ImageList with your own images. One
use of all this information is to create a ListBox that shows the installed
drives, drawing their icons, display names, and types.
The les referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103BS.
A Brazilian, Bruno Sonnino has been developing with Delphi since its first
version in 1995. He has written the books 365 Delphi Tips and Developing
Applications in Delphi 5, published in Portuguese. He can be reached at
sonnino@netmogi.com.br.
nstallShield Professional - Windows Installer Edition 2.0 is the latest tool from
InstallShield Corp. for building installations that use Microsofts Windows Installer
service. Windows Installer offers two ways to create an installation: manually through a
series of views, or the Project Wizard can get you started.
Figure 1 shows the Windows Installer window
immediately after startup. The Windows Installer
user interface mimics Outlook in its single window
and its organization in a series of views within the
window. If you dont like the tree view shown in
the left-hand panel, you can display an Outlook
bar (called a viewbar in the Windows Installer
manual) in addition to, or in place of, the tree view.
To start a new project, click Create a new project in
the center panel; the right panel will change to offer
four options. The rst, Blank Merge Module Project,
from the Help menu. This is critical for those new to Windows
Installer. All new users will want to read the introductory sections in
the Windows Installer help to get an understanding of the purpose of
Windows Installer, how it works, and how installations are organized.
Conclusion
If you need a powerful tool to build complex Windows Installer
setups, you wont go wrong with InstallShield Professional Windows Installer Edition 2.0. The Outlook-like user interface
will be familiar to most users and provides easy access to the products vast array of features. If you dont need the sophistication of
InstallShield Professional - Windows Installer Edition 2.0, take a
look at InstallShield Express at http://www.installshield.com.
Other Features
Windows Installer includes a debugger that lets you set breakpoints
and step through the user interface sequence of actions. If you use
a source code version control system that supports Microsofts SCC
interface, Windows Installer will integrate with it to analyze the
difference between versions and create updates. Supported source
control systems include Visual SourceSafe, Rational ClearCase, and
PVCS. If you build a single le setup for Web distribution, Windows
Installer can protect it with a password or digital signature. One
of the most intriguing features of Windows Installer is that it is
an Automation server. This lets you write Automation clients that
use Windows Installer to create or modify setups. An editor is also
included which lets you edit the Windows Installer tables that cannot
be maintained through the user interface.
Documentation
The documentation is excellent. It includes a Getting Started manual,
extensive online help, and an online tutorial. Be sure to read the
manuals nal chapter, Frequently Asked Questions, which shows
you how to accomplish important tasks that arent easy to nd in
the user interface. You can also take advantage of the 30 days of free
technical support that comes with the product. The documentation
also includes the Windows Installer SDK help le, which is accessible
41 March 2001 Delphi Informant Magazine
Bill Todd is president of The Database Group, Inc., a database consulting and
development firm based near Phoenix. He is co-author of four database programming books, author of more than 60 articles, a contributing editor to Delphi
Informant Magazine, and a member of Team Borland, providing technical support
on Borland Internet newsgroups. He is a frequent speaker at Borland Developer
Conferences in the US and Europe. Bill is also a nationally-known trainer and has
taught Delphi programming classes across the country and overseas. Bill can be
reached at bill@dbginc.com.
TextFile
constants, and basic input and output. Chapters 5 and 6 focus on program ow, and
include cogent discussions of the case statement, if statements, sets, and various types
of loops. Chapter 7 introduces procedures
and functions, but unfortunately does not discuss very many of the built-in procedures
and functions. Most of these have been a
part of Turbo Pascal from the early days. The
discussion about working with strings would
have been enhanced by including copy, pos,
and the anachronistic concat routine. I recommend that if Rachele decides to write a
new edition, he consider adding a chapter
on built-in procedures and functions or incorporating a discussion of these in appropriate
chapters. This would round out an otherwise
excellent introduction to Object Pascal.
Chapters 9 and 10 introduce the
languages important data collections, arrays,
and records. As in previous chapters, the
author introduces these topics systematically,
with easy-to-follow descriptions, and builds
on that foundation by providing important
warnings and helpful examples. The nal two
chapters broach the more advanced topics of
object-oriented programming, pointers, and
le handling. The presentation on pointers
includes examples of building linked lists,
and the use of dynamic variables. This
extremely clear and accessible presentation of
these more advanced Object Pascal topics is
one of the highlights of this book.
File | New
Directions / Commentary
s Vice President of Developer Relations, David Intersimone (or David I for short) is responsible for building full-service
developer programs offering corporations, government agencies, third-party software developers, consultants, and
end-users the support they need to be successful with Borland products. This brings him into contact with professional
programmers, user groups, the technical press, book authors, and corporate customers around the world.
Delphi Informant: Youre arguably the best known certainly the most
recognizable person at Borland. How did you come to join Borland,
and what are some of your more memorable experiences there?
David I: Ive been here over 15 years now. A lot has happened over
the years too much to cover in just one answer, or even in an
article. Maybe Ill write a book someday. But Ill try to cover a
few highlights.
Before I joined Borland in 1985, I worked for Softsel Computer
Products (now called Merisel). I met Philippe Kahn at Comdex Las
Vegas in 1983. He came by the Softsel booth where I was working
and we started talking. He said he had a Pascal compiler, but was
not looking for a distributor. I told him about an internal project
I was building using Microsoft Pascal. He gave me the CP/M and
PC/DOS versions of Turbo Pascal 1.0 and went on his way. I kept
track of what Borland was doing for the next two years. A coworker at Softsel, Spencer Leyton, joined Borland and convinced
me to come up to Scotts Valley and interview with Philippe. My job
interview was out on a sailboat in Monterey Bay. I started working
for Borland, looking for new products for the company. My next
job was Director of R&D for Language products. During my time
in R&D we brought out Turbo Pascal for the Macintosh, Turbo
Pascal 4 and 5, Turbo C 1.5 and 2, Turbo Assembler and Debugger,
Turbo Basic, and Turbo Prolog.
For the past 9 years, Ive been chief evangelist for our developer
products. I have a wonderful job traveling around the world, talking
to programmers, meeting customers, launching products, speaking at
conferences, writing articles, and doing everything else I can to help
our company.
What are some of the memorable experiences Ive had? Its so hard
to single a few out. Being part of the changing face of programming,
improving the process of programming, and making the job easier for
programmers are some of my best memories. Its great to be able to
work with the best developer tool smiths on the planet.
43 March 2001 Delphi Informant Magazine
File | New
Having all of our products available on Linux is one of those business
opportunities. Continuing to support and take advantage of new
Microsoft Windows capabilities also helps fuel our growth. Being
very aggressive with the free JBuilder Foundation Edition to seed the
market, and seeing the benets with sales of JBuilder and Borland
AppServer have helped our business.
DI: Many of us are excited about the Kylix Project and what Borlands support for Linux development will mean to the entire community. What are some of your observations and predictions on how
this will affect the company?
David I: Our lawyers have asked me to include the following
Forward-looking statements.... We cant make predictions about
the future. However, Project Kylix is affecting our company today.
Weve been showing the product at trade shows and conferences
where weve never been before. Weve been interviewed and written up in Linux magazines and Web sites. Project Kylix has
already won a few awards. Kylix Beta won the Best Corporate
Product award at Interact + IT 2000 in Melbourne, Australia.
Kylix Beta also won the Nikkei-Byte Editors Choice award.
Nikkei-Byte is one of the most popular developer magazines in
Japan, with a circulation of 80,000.
We are excited about Project Kylix and support for Linux across our
entire product line. Project Kylix will bring an instant army of
skilled application developers for Linux. It will allow the millions of
Delphi, C++Builder, and Visual Basic developers to develop desktop,
database, and Web applications. It will bring to Linux the hundreds
of thousands of applications and components built with Delphi.
If you love Delphi, go and tell others to try it and see what theyre
missing. If each of you can convince 50 or 100 developers to
switch to Delphi and/or Kylix, well be even more successful as a
company, and well be able to provide more products, features, services, and support to you. Keep the feedback coming. Go Delphi!
Go Borland!
We added coverage of Linux for the rst time at our conference this
past summer. You will denitely see more coverage of Linux at our
conference in Long Beach, July 21-25, 2001 [for more information
go to http://www.borland.com/conf2001].
DI: Borland held a Kylix Kickoff event to which you invited
third-party developers, Project JEDI representatives, and others. I was
delighted to see this early involvement of these folks who contribute
greatly to the community. What results can we expect in terms of
early third-party support for Kylix?
David I: Weve been working with hundreds of third-party companies since early 2000. We invited many of our supporting tool
44 March 2001 Delphi Informant Magazine