On The Cover: September 2001, Volume 7, Number 9
On The Cover: September 2001, Volume 7, Number 9
On The Cover: September 2001, Volume 7, Number 9
ON THE COVER
5
OP Tech
FEATURES
10 On Language
Console Applications: Part II Mike Edenfield
In Part II of his console programming series, Mike Edenfield shares
several advanced features that console applications offer programmers, including screen buffers, view windows, and console modes.
16 In Development
Custom Key Bindings Cary Jensen, Ph.D.
Cary Jensen introduces custom key bindings, a little-known feature of
the Delphi Open Tools API that permits you to add custom keystroke
combinations to the Code editor. And it works for Kylix too!
21 Columns & Rows
Using the XML Features of SQL Server 2000: Part II
Alex Fedorov
This month, Alex Fedorov describes how Delphi interacts with the
Microsoft XML Parser, before moving on to the main topic of how to
query SQL Server 2000 for XML data from Delphi.
25 Kylix Tech
Kylix Program Launcher Brian Burton
Brian Burton demonstrates a pure Kylix alternative to using shell
scripts to launch a Linux application that requires shared libraries,
without requiring technical finesse from the user.
28
Greater Delphi
32
Sound+Vision
REVIEWS
37
ExpressBars Suite 4
Product Review by Ron Loewy
40
43
44
44
45
UML Explained
Book Review by Alan C. Moore, Ph.D.
45
SQL in a Nutshell
Book Review by Christopher R. Shaw
DEPARTMENTS
2
46
48
Delphi Tools
Best Practices by Clay Shannon
File | New by Alan C. Moore, Ph.D.
Delphi
T O O L S
New Products
and Solutions
ing capabilities.
Check the ModelMaker Web site
for a free fully functional demonstration of ModelMaker 6.0.
ModelMaker Tools
Price: Single-user license, US$269;
10-user license, US$995; site license,
US$1,995.
Contact: info@modelmakertools.com
Web Site: http://www.modelmakertools.com
Delphi
T O O L S
New Products
and Solutions
Delphi
T O O L S
New Products
and Solutions
OP Tech
By Bill Todd
bExpress is one of the major new features in Delphi 6. This article takes a look
at how to build and deploy a two-tier client/server application using dbExpress
and the sample InterBase database that ships with Delphi. [For an introduction to
dbExpress, its place vis--vis Delphi 6 and Kylix, deployment, and similar issues, see
Bill Todds article dbExpress in the March 2001 Delphi Informant Magazine.]
If you have built applications using the MIDAS
components in a prior version of Delphi, youll
nd yourself on familiar ground when building
your rst dbExpress application. If you havent
used MIDAS, your rst dbExpress application
will involve a lot of new concepts. The best way
to get through the basics is to walk through building the sample application that accompanies this
article (see end of article for download details).
OP Tech
procedure TEmpDm.LoadIniSettings;
var
Ini: TIniFile;
begin
Ini := TIniFile.Create(ExtractFilePath(
Application.ExeName) + 'Emp.ini');
try
EmpConn.Params.Values['Database'] :=
Ini.ReadString('Database', 'Database', '');
finally
Ini.Free;
end;
end;
property to the SQL statement you want to use. In this case the
SQL statement is:
SELECT * FROM EMPLOYEE
WHERE DEPT_NO = :DEPT_NO
Although you dont need to change it, take note of the CommandType
property. This property controls what you can enter in CommandText.
When set to its default value of ctQuery, CommandText holds
the SQL statement you want to execute. When CommandType is
ctTable, CommandText provides a drop-down list of table names;
when CommandType is ctStoredProc, CommandText contains a
drop-down list of stored procedure names. Add a DataSource
component and connect it to the EmpDs SQLDataSet and name
it EmpLinkSrc. Add a second SQLDataSet component and set its
name to SalesDs. Then set its SQLConnection property and its
CommandText property to:
Figure 3: The SQLConnection components Params property.
Set the SalesDs DataSource property to the EmpLinkSrc DataSource component. This will cause the SalesDs SQLDataSet to get
the value of its :EMP_NO parameter from the current record in
EmpDs, and also cause the Sales records to be embedded in the
Employee dataset as a nested dataset eld. Next, add a DataSetProvider
from the Data Access page of the Component palette, and set its
DataSet property to the Employee SQLDataSet, EmpDs.
OP Tech
procedure TMainForm.LoadDeptCombo;
begin
with EmpDm.DeptDs do begin
Open;
while not EOF do begin
DeptCombo.Items.Add(FieldByName('DEPT_NO').AsString +
' - ' + FieldByName('DEPARTMENT').AsString);
Next;
end;
Close;
end;
end;
on the main form. To display the records from the Sales table,
add another ClientDataSet and DataSource to the data module,
naming them SalesCds and SalesSrc respectively. Select the
Employee ClientDataSet, double-click it to open the Fields editor,
then right-click and choose Add All Fields. Note the last eld in the
Fields editor is named SalesDs, the same name as the SQLDataSet
component for the Sales table. This is the nested dataset eld that
contains the Sales records linked to each record in the Employees
table. Now select the SalesDs ClientDataSet, click its DataSetField
property, and click the arrow to open the drop-down list. The
only entry in the list will be EmpCdsSalesDs, assuming that you
used the component names shown in Figure 1. The last step is to
set the DataSet property of the DataSource component to connect
it to the Sales ClientDataSet.
This application uses typical client/server architecture by forcing
the user to provide some selection criteria thats used to fetch a
small set of records. In this case, the selection criteria is provided
by choosing a department from the combo box at the top of the
form. The combo box is loaded with the department numbers and
names when the application starts. The data module in Figure 1
contains a SQLDataSet component named DeptDs thats used to
load the combo box.
Figure 5 shows the LoadDeptCombo method thats called from
the main forms OnCreate event handler. This code opens the
department SQLDataSet and uses a while loop to iterate through
the records and load the combo box. Note that no Provider
or ClientDataSet component is required in this case, because
the dataset is only being read and traversed from beginning to
end. Because this SQLDataSet isnt being used with a Provider
and ClientDataSet, its NoMetaData property is set to True. This
improves performance, because metadata information required for
updates isnt retrieved from the server.
When the application starts, the main forms OnCreate event
handler calls a custom method that opens both the employee
and sales ClientDataSets. Since no values have been assigned to
the parameters in the SQL statements, no records are returned,
and both grids are empty. When the user chooses a department
from the combo box, the combo boxs OnChange event handler,
shown in Figure 6, executes. This code closes the Employee
ClientDataSet, extracts the department number from the rst
three characters of the combo boxs Text property, and assigns
it to the ClientDataSets DEPT_NO parameter. When the
ClientDataSet is opened, it sends this new parameter value
through the provider to the SQLDataSet for the Employee table,
opens the SQLDataSet, retrieves the records returned by the
query, and closes the SQLDataSet.
7 September 2001 Delphi Informant Magazine
How It Works
If youve worked with MIDAS before, you can skip this section. If
you havent, heres a brief explanation of how the provide/resolve
architecture implemented by the dbExpress components works.
When you open a ClientDataSet, it sends a request to its provider
requesting data. The provider, in turn, opens the dataset its connected to and retrieves all the rows supplied by that data set. The
provider stores the data in a variant in a proprietary format and
sends the data packet to the ClientDataSet. The ClientDataSet
receives the data packet from the provider, extracts the data, and
loads it into memory.
When you edit the ClientDataSet data, the changes are stored
in memory and a property named Delta. To update the database
you must call the ClientDataSets ApplyUpdates method. In the
OP Tech
Getting the Database Schema
Figure 7 shows the Schema tab of the applications main form.
The Schema Type combo box at top of the form lets you choose
to view Tables, System Tables, or Stored Procedures in the top grid.
The choices in the Schema Details combo box vary depending on
whether youre viewing schema information for tables or stored
procedures. For tables or system tables, you can view information
on columns or indices. If youre viewing stored procedures, the
Schema Details combo box contains a single choice, Procedure
Parameters. Schema information is retrieved using the SQLDataSet
component and its SetSchemaInfo method.
OP Tech
procedure TEmpDm.UpdateSchema(SchemaType: Integer);
begin
with SchemaDs do begin
SchemaCds.Close;
case SchemaType of
StoredProcs:
begin
SetSchemaInfo(stProcedures, '', '');
MainForm.LoadSchemaDetail(sdtProcedure);
end;
SysTables:
begin
SetSchemaInfo(stSysTables, '', '');
MainForm.LoadSchemaDetail(sdtTable);
end;
Tables:
begin
SetSchemaInfo(stTables, '', '');
MainForm.LoadSchemaDetail(sdtTable);
end;
end; // case
SchemaCds.Open;
end; // with
end;
Figure 8: This procedure is called by the OnChange event handler for the Schema Type combo box.
Conclusion
dbExpress makes it easy to create and deploy applications that
use SQL database servers. Since all of the database connection
information is contained in the properties of the SQLConnection
component, you have complete control over whether this information is embedded in your application and stored in the registry, an
9 September 2001 Delphi Informant Magazine
procedure TMainForm.LoadSchemaDetail(
DetailType: TSchemaDetailType);
begin
with SchemaDetCombo do begin
Items.Clear;
case DetailType of
sdtTable:
begin
Items.Add(Columns);
Items.Add(Indices);
end;
sdtProcedure: Items.Add(ProcParams);
end;
end;
end;
INI le, a text le, or somewhere else. This also lets you provide
your own user interface for changing these properties.
dbExpress also makes writing an application that supports multiple
database servers easy. All you have to do is supply the driver DLL
for the database server your client is using and set the DriverName,
LibraryName, and VendorLib properties of the SQLConnection
component to the correct values for that database server. dbExpress
is small, fast, and easy to deploy, making it an excellent tool for
writing database applications.
The sample project referenced in this article is available on the Delphi
Informant Magazine Complete Works CD located in INFORM\2001\
SEP\DI200109BT.
On Language
By Mike Edenfield
Console Applications
Part II: Advanced I/O
ast month, in Part I of this series, we covered the basics of writing applications that
make use of the Windows console (character-mode) subsystem. This subsystem
user interface allows you to write command-line utilities and applications that look
and behave like applications written for the MS-DOS operating system.
This is only the beginning of what console applications can do, however. Far from being restricted
to old, pre-Windows programming techniques, the
Low-level Output
The console windows output consists of a collection of character cells. Each cell has a character
and an attribute associated with it. Internally, Windows treats the output as a two-dimensional array
of CHAR_INFO records. Several low-level output
functions exist that allow you to act on a range of
these cells at once (see Figure 1).
The first two functions act on a rectangular region
of the screen. The destination buffer pointer is filled
with an array of CHAR_INFO structures containing the cells at the requested region. The remaining
functions act on a continuous series of these characters, returning either an array of characters or
attributes. Wrapping is automatic at the end of the
line. If the bottom of the screen is reached, the
functions cease processing and return the number
of characters written in the last var parameter.
Use of these functions is fairly straightforward. The
sample programs in Listings One and Two (beginning on page 14) demonstrate a common use of
the FillConsoleOutputCharacter function: clearing
the screen. This is done by simply filling the entire
screen, starting at 0,0, and proceeding for (height *
On Language
Until now, weve been working under a few assumptions. All of our
programs act as if:
there were already a single console present, and we had to use it;
there were only one screen of output available; and
the output screen was only as big as the console window.
These three assumptions are the defaults for new console applications,
but by no means are they the only options. Processes are free to
detach themselves from their parents console and create their own,
as well as create multiple output buffers and various-sized windows
into those output buffers. Taking advantage of these features requires
an understanding of the three different Windows objects involved in
displaying text to a console.
Contained within a console are its input and output buffers. Each
console has a single input buffer that handles all input events. However,
theres no such limit to the number of output buffers, which are referred
to as screen buffers. There are two-dimensional grids of characters
representing whats to be displayed on the console. A single screen buffer,
the same size as the console window, is created automatically. The API
calls to manipulate screen buffers are shown in Figure 3.
You can create as many screen buffers as you want, but only one of
them can be active at any given time. The various console output
functions all take a handle to a screen buffer, which doesnt have to
be the active screen buffer. The standard output handle retrieved from
GetStdHandle, however, will always refer to whichever console buffer
is active at the time its called.
function CreateConsoleScreenBuffer(
dwDesiredAccess, dwShareMode: DWORD;
lpSecurityAttributes: PSecurityAttributes;
dwFlags: DWORD; lpScreenBufferData: Pointer):
THandle; stdcall;
function GetConsoleScreenBufferInfo(
hConsoleOutput: THandle;
var lpConsoleScreenBufferInfo: TConsoleScreenBufferInfo):
BOOL; stdcall;
function SetConsoleActiveScreenBuffer(
hConsoleOutput: THandle): BOOL; stdcall;
function SetConsoleScreenBufferSize(
hConsoleOutput: THandle; dwSize: TCoord): BOOL; stdcall;
function ScrollConsoleScreenBuffer(hConsoleOutput: THandle;
const lpScrollRectangle: TSmallRect;
lpClipRectangle: PSmallRect; dwDestinationOrigin: TCoord;
var lpFill: TCharInfo): BOOL; stdcall;
On Language
In Part I, we discussed two levels of input and output processing
available to consoles. Microsoft terms these as high-level and
low-level access to the console. Not surprisingly, there are different sets of console-mode options affecting the high- and low-level
I/O functions. Theyre set or cleared by passing a bit mask to the
SetConsoleMode function, which can contain any combination of
high- and low-level I/O mode options.
Console Modes
Much of the behavior of the console window depends on what mode
Windows has put the console in. For most situations, the default
settings for the console mode will be sufcient. However, changing
these modes allows you a very intimate level of control over the
behavior of the console. Changing and querying the console modes is
accomplished via two API calls:
function GetConsoleMode(hConsoleHandle: THandle;
var lpMode: DWORD): BOOL; stdcall;
function SetConsoleMode(hConsoleHandle: THandle;
dwMode: DWORD): BOOL; stdcall;
For high-level input and output, the following console modes are
available:
ENABLE_LINE_INPUT Input is processed by your application one line at a time.
ENABLE_ECHO_INPUT Characters are echoed back to the
screen as entered.
ENABLE_PROCESSED_INPUT Windows handles editing
and control characters internally.
ENABLE_PROCESSED_OUTPUT Windows handles control sequences automatically.
ENABLE_WRAP_AT_EOL_OUTPUT Lines wrap automatically at the edge of the screen buffer.
By default, all ve of these options are turned on. This mode of operation
is sometimes called cooked mode, because Windows performs most
of the editing and control-code handling for you. Your ReadFile calls
will block until a carriage return is entered, and Windows will correctly
process and echo back any pressed editing or control keys. If you want
to turn off the three input modes, keep in mind that ECHO_INPUT
mode is disabled automatically if you disable LINE_INPUT. Also,
attempts to turn off WRAP_AT_EOL_OUTPUT mode under Windows 95/98 are silently ignored.
Low-level console output, using functions such as WriteConsoleOutputCharacter
or FillConsoleOutputAttribute, isnt affected by any input or output
modes. This low-level output is always in a very raw, direct form.
However, the ReadConsoleInput API and related low-level input functions are affected by these three input modes:
ENABLE_MOUSE_INPUT determines if MOUSE_EVENT
input events are reported to your application.
ENABLE_WINDOW_INPUT determines if
WINDOW_BUFFER_SIZE_EVENT input events are reported
to your application.
ENABLE_PROCESSED_INPUT automatically handles CC.
Processed input mode here simply means that Windows automatically calls the control handler when CC is pressed. By default,
processed and mouse input are on, but window input is off. Note
that no matter what console mode you are in, youll always receive
KEY_EVENT, FOCUS_EVENT, and MENU_EVENT messages.
In practice, youll rarely need to change these modes. While turning
off the default input modes for high-level input gives you much
ner control over console input, you can achieve the same effect
simply by using the low-level input functions. And, as we are about
to see, its a simple task to override the default CC handler to
do what you want.
On Language
ag isnt specied, any child processes of a console process belong
to the same group. Console applications can then send signals to a
specied process group, using the following API call:
function GenerateConsoleCtrlEvent(dwCtrlEvent: DWORD;
dwProcessGroupId: DWORD): BOOL; stdcall;
This function will send the specied signal to the console window
associated with the calling process. Any other applications that are
both attached to that console and part of the given process group will
receive this signal. This can be used by a parent process to control the
behavior of multiple child processes at once.
Figure 5: The control-handlers example program at run time.
function SetConsoleCtrlHandler(
HandlerRoutine: TFNHandlerRoutine; Add: BOOL):
BOOL; stdcall;
The sample program in Listing Two only deals with the more basic
behavior of signal handlers. (Its shown at run time in Figure 5.) We
simply install a pair of control handlers and have them demonstrate
the effect of the return values on Windows behavior. As you can see by
the behavior of this sample, if none of the other handlers return True
to indicate theyve handled a given signal, the internal default handler
is still executed last. Its behavior is to simply kill the process whenever
any of the control signals are received. If this isnt the behavior you
want, you must make sure to install a control handler that returns True
as soon as possible after your program begins executing.
Conclusion
In this installment of the console programming series, we went over
the advanced features console applications provide to programmers.
Weve now covered nearly every aspect of console programming and
the console APIs, and observed how to take very low-level control of
the console and its associated buffers.
However, console programming is most effective when used with
the rest of the Windows API. In the nal article in this series,
next month, well examine how to use other common Windows
programming techniques in conjunction with console applications.
In particular, well examine how to use a console within a program
thread, create a message queue that a GUI-less application can use,
use consoles and graphical windows in the same application, and
replace the standard input and output handles with handles of our
own, including other le handles and anonymous pipes, to perform
I/O redirection from within a Windows application.
The sample programs referenced in this article are available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
2001\SEP\DI200109ME.
The last three events, the close, logoff, and shutdown messages,
exhibit special behavior depending on the results of the handler
functions. If all of the handler functions return False, the process
will exit as soon as the last handler is done. However, if any of the
handler functions return True, Windows will instead prompt the user
to conrm that the process should be shut down. The user then
has the option to terminate the process immediately, or prevent the
process (and in turn, Windows itself ) from shutting down. Also, if a
handler function takes too long to return during one of these events,
Windows will automatically give the user the option to close the
process, cancel the shutdown, or continue to wait.
When several applications are attached to the same console, each of
them receives the signals sent to that console window, and each of
their handlers has a chance to process that signal for its own process.
Additionally, you can collect multiple console applications as part of a
console process group. This is done when creating a new console process with CreateProcess. If the CREATE_NEW_PROCESS_GROUP
13 September 2001 Delphi Informant Magazine
On Language
Begin Listing One ScreenBuffers Program
program ScreenBuffers;
uses
Windows, SysUtils;
var
hInput, hOriginal, hSecond, hActive: THandle;
arrInputRecs : array[0..9] of TInputRecord;
dwCount, dwCur : DWORD;
bQuit : Boolean = False;
coorNew : TCoord = (X:100; Y:100);
rectView: TSmallRect =
(Left:0; Top:0; Right:99; Bottom:49);
const
OUTPUT_STRING =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()[]{}';
FOREGROUND_MAGENTA = FOREGROUND_RED or FOREGROUND_BLUE;
FOREGROUND_BR_MAGENTA =
FOREGROUND_MAGENTA or FOREGROUND_INTENSITY;
procedure WriteToScreen(buf: String);
var
dwCount: DWORD;
begin
buf := buf + #13#10;
WriteConsole(hActive, PChar(@buf[1]), Length(buf),
dwCount, nil);
end;
procedure FillOutputBuffer(hBuf: THandle);
var
dwAttr, dwCur: DWORD;
begin
dwAttr := 0;
for dwCur := 0 to 100 do begin
SetConsoleTextAttribute(hSecond, dwAttr + 1);
WriteConsole(hSecond, PChar(@OUTPUT_STRING[1]),
Length(OUTPUT_STRING), dwCount, nil);
dwAttr := (dwAttr + 1) mod 15;
end;
Writeln('Secondary buffer populated with data.');
end;
procedure ClearOutputBuffer(hBuf: THandle);
var
cbsi: TConsoleScreenBufferInfo;
coorClear: TCoord;
dwWritten: DWORD;
cFill: Char;
begin
GetConsoleScreenBufferInfo(hBuf, cbsi);
coorClear.X := 0;
coorClear.Y := 0;
cFill := ' ';
FillConsoleOutputCharacter(hBuf, cFill,
cbsi.dwSize.X * cbsi.dwSize.Y, coorClear, dwWritten);
end;
procedure ScrollViewWindow(hBuf: THandle; bUp: Boolean);
var
rectNew: TSmallRect;
begin
if bUp then
rectNew.Top := -1
else
rectNew.Top := 1;
rectNew.Bottom := rectNew.Top;
rectNew.Left := 0;
rectNew.Right := 0;
SetConsoleWindowInfo(hBuf, False, rectNew);
end;
procedure SwitchConsole;
begin
if hActive = hOriginal then
begin
SetConsoleActiveScreenBuffer(hSecond);
hActive := hSecond;
end
else
begin
SetConsoleActiveScreenBuffer(hOriginal);
hActive := hOriginal;
end;
end;
begin
{ First, release our existing console,
and make a new one. }
FreeConsole;
AllocConsole;
SetConsoleTitle('Screen Buffers Demo');
{ Get a handle to the auto-created input
and output buffers. }
hInput := GetStdHandle(STD_INPUT_HANDLE);
hOriginal := GetStdHandle(STD_OUTPUT_HANDLE);
hActive := hOriginal;
SetConsoleTextAttribute(hActive, FOREGROUND_BR_MAGENTA);
WriteLn('Got Standard Output Handle.');
{ Create a second screen buffer. }
hSecond := CreateConsoleScreenBuffer(GENERIC_READ or
GENERIC_WRITE, 0, nil, CONSOLE_TEXTMODE_BUFFER, nil);
{ *** Windows 95/98: Comment out from here... }
if not SetConsoleScreenBufferSize(hSecond, coorNew) then
WriteLn('error: SetConsoleScreenBufferSize() == ',
GetLastError)
else
WriteLn('Adjusted secondary screen buffer size.');
if not SetConsoleWindowInfo(hSecond, True, rectView) then
WriteLn('error: SetConsoleWindowInfo() == ',
GetLastError)
else
WriteLn('Adjusted secondary screen buffer window.');
{ *** ...to here. }
FillOutputBuffer(hSecond);
{ Process the input loop here. }
while not bQuit do begin
ReadConsoleInput(hInput, arrInputRecs[0], 10, dwCount);
for dwCur := 0 to dwCount - 1 do begin
case arrInputRecs[dwCur].EventType of
KEY_EVENT:
with arrInputRecs[dwCur].Event.KeyEvent do
if bKeyDown then
if (dwControlKeyState and
ENHANCED_KEY) > 9 then
case wVirtualKeyCode of
VK_UP
:
ScrollViewWindow(hActive, True);
VK_DOWN :
ScrollViewWindow(hActive, False);
end
else
case Ord(AsciiChar) of
13: SwitchConsole;
Ord('?'):
begin
WriteLn('Console commands: ');
WriteLn(' ?
- This Help Summary');
WriteLn(' C
- Clear secondary output buffer.');
WriteLn(' F
- Fill secondary output buffer.');
WriteLn(' Q
- Quit program.');
WriteLn(' <ent> - Toggle active screen buffer.');
On Language
WriteLn('
WriteLn('
WriteLn('OFF');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
WriteLn('Console Handler 1 has fired ',
nHandler1, ' times.');
WriteLn('Console Handler 2 has fired ',
nHandler2, ' times.');
end;
function Handler1(dwSignal: DWORD): BOOL; stdcall;
begin
Inc(nHandler1);
PaintScreen;
Result := bHandler1;
end;
function Handler2(dwSignal: DWORD): BOOL; stdcall;
begin
Inc(nHandler2);
PaintScreen;
Result := bHandler2;
end;
begin
hStdInput := GetStdHandle(STD_INPUT_HANDLE);
hStdOutput := GetStdHandle(STD_OUTPUT_HANDLE);
{ Register them in reverse order so they
fire Handler1 then Handler2. }
SetConsoleCtrlHandler(@Handler2, True);
SetConsoleCtrlHandler(@Handler1, True);
ClearOutputBuffer;
PaintScreen;
while True do begin
ReadConsoleInput(hStdInput, arrInputRecs[0], 10,
dwCount);
for dwCur := 0 to dwCount - 1 do
case arrInputRecs[dwCur].EventType of
KEY_EVENT:
with arrInputRecs[dwCur].Event.KeyEvent do begin
cCur := AsciiChar;
if (not bKeyDown) and (cCur = '1') then
begin
bHandler1 := not bHandler1;
PaintScreen;
end;
if (not bKeydown) and (cCur = '2') then
begin
bHandler2 := not bHandler2;
PaintScreen;
end;
end;
end;
end;
end.
In Development
here is a powerful feature of the Delphi and Kylix Code editor that permits you to
add your own custom keystrokes. This little-known feature is referred to as custom
key bindings, and is part of the Open Tools API. The Open Tools API (OTA) provides
a collection of classes and interfaces you can use to write your own extensions to
the Delphi and Kylix IDE.
This article gives you an overview of this interesting feature, and provides a simple key binding class
that you can use as a starting point for creating
your own custom key bindings. This key binding
class, which I am calling the duplicate line key
binding, makes a duplicate or copy of the current
Figure 1: The Key Mappings page of the Editor Properties dialog box.
In Development
IOTAKeyboardBinding = interface(IOTANotifier)
['{F8CAF8D7-D263-11D2-ABD8-00C04FB16FB3}']
function GetBindingType: TBindingType;
function GetDisplayName: string;
function GetName: string;
procedure BindKeyboard(
const BindingServices: IOTAKeyBindingServices);
property BindingType: TBindingType read GetBindingType;
property DisplayName: string read GetDisplayName;
property Name: string read GetName;
end;
As you can see, this interface declares four methods and three
properties. Your key binding class must implement the methods.
Note, however, that it does not need to implement the properties.
(This is a regular source of confusion when it comes to interfaces,
but the fact is that the properties belong to the interface and
are not required by the implementing object. Sure, you can implement the properties in the object, but you dont have to. I didnt
in this example.)
In addition to the methods of the IOTAKeyboardBinding interface,
your key binding class must include one additional method for
each custom keystroke you want to add to the editor. To be
compatible with the AddKeyBinding method used to bind these
additional methods, the methods must be TKeyBindingProc type
methods. The following is the declaration of the TKeyBindingProc
method pointer type, as it appears in the ToolsAPI unit:
TKeyBindingProc = procedure (const Context: IOTAKeyContext;
KeyCode: TShortcut; var BindingResult: TKeyBindingResult)
of object;
This declaration indicates that the additional methods you write, each of
which adds a different keystroke combination to the editor, must take
three parameters: IOTAKeyContext, TShortcut, and TKeyBindingResult.
Figure 3 shows the key binding class declared in the DupLine.pas unit.
This class, named TDupLineBinding, includes only one new key binding.
In Development
procedure TDupLineBinding.DupLine(
const Context: IOTAKeyContext; KeyCode: TShortcut;
var BindingResult: TKeyBindingResult);
var
ep: IOTAEditPosition;
eb: IOTAEditBlock;
r, c: Integer;
begin
try
ep := Context.EditBuffer.EditPosition;
ep.Save;
// Save current cursor position.
r := ep.Row;
c := ep.Column;
eb := Context.EditBuffer.EditBlock;
ep.MoveBOL;
eb.Reset;
eb.BeginBlock;
eb.Extend(EP.Row+1,1);
eb.EndBlock;
eb.Copy(False);
ep.MoveBOL;
ep.Paste;
// Restore cursor position.
ep.Move(r, c);
finally
ep.Restore;
end;
BindingResult := krHandled;
end;
where the rst parameter is the ANSI value of the keyboard character,
and the second is a set of zero, one, or more as TShiftState. The
18 September 2001 Delphi Informant Magazine
As you can see, this BindKeyboard implementation will associate the code
implemented in the DupLine method with the CD combination.
In Development
can help you learn how to implement your key bindings. The
code shown in Figure 4 implements TKeyBindingProc from the
TDupLineBinding class.
In Development
4) Set the Package file name to the name you want to give your
package. In Figure 6, this le is named keybin.dpk, and is
being stored in the \Lib directory where Delphi 6 is installed.
(In Linux, a good place to store this le is the hidden .Borland
directory, located in the users home directory.) Also provide
a brief description for the package in the Package description
eld.
5) Click OK. A dialog box is displayed to inform you that it will
build the package and install it.
6) Click Yes. After creating, compiling, and installing the package, you will see a dialog box conrming that the package is
installed.
7) Click OK. The installed package now appears in the Package
editor, as shown in Figure 7.
8) Close the Package editor, making sure to click Yes when it asks
if you want to save changes to the package project.
Your key binding is now available. It will now appear in the Key
Mappings page of the Editor Properties dialog box, as shown in
Figure 8. Your installed key binding is now available to all editor
key mappings. If you want to disable your key binding, uncheck
the check box that appears next to its name in the Enhancement
modules list box.
Note: If you inspect Figure 6, you will see that I stored
DupLine.pas in the Borland\components directory. This permits
me to easily share components between multiple versions of
Delphi, and also to re-install a version of Delphi and delete its
directory without losing my custom components. To use this directory, however, I had to add it to the Library path list on the Library
page of the Environment Options dialog box. Display this dialog
box by selecting Tools | Environment Options.
Conclusion
procedure Register;
begin
(BorlandIDEServices as IOTAKeyboardServices).
AddKeyboardBinding(TDupLineBinding.Create);
end;
Cary Jensen is president of Jensen Data Systems, Inc., a Houston-based training and consulting company. He is the author of 18 books, including
Building Kylix Applications (Osborne/McGraw-Hill), Oracle JDeveloper
(Oracle Press), and Delphi In Depth (Osborne/McGraw-Hill). Cary
is also Contributing Editor to Delphi Informant Magazine, and an internationally respected trainer and speaker. For information about Carys
on-site training, public Delphi and Kylix seminars, and consulting
services, please visit http://www.jensendatasystems.com, or e-mail Cary at
cjensen@jensendatasystems.com.
By Alex Fedorov
elphi has a long history of database support, from the BDE in Delphi 1 and subsequent
versions, to the ADOExpress components that implement the ADO- and OLE DB-based
data access in Delphi 5 and 6. Now, with the XML features of Microsoft SQL Server 2000,
we can talk about a third generation of data access in Delphi one thats compatible
with main IT trends.
Last month, you were introduced to the new features of Microsoft SQL Server 2000 that make it
an XML-enabled database server, e.g. the ability
to access SQL Servers using the HTTP protocol,
support for XDR (XML-Data Reduced) schemas,
the ability to specify XPath queries against these
schemas, and the ability to retrieve and write XML
data. You also learned that XML-based data extraction is implemented in SQL Server 2000 by the
FOR XML clause thats part of the SELECT statement. The ability to query Microsoft SQL Server
directly without extra data access components
and receive data represented as XML documents
opens new possibilities for Delphi developers.
To parse XML documents generated by querying
SQL Server, well naturally need a parser. And since
were using a Microsoft database, the best choice
is the Microsoft XML Parser, which provides a lot
of objects, methods, and properties to manipulate
XML documents. So well begin this months discussion by describing how Delphi interacts with the
Microsoft XML Parser, before moving on to the
Old Name
New Name
Where
type
implementation
type
var
type
type
type_
implementation_
type_
var_
type_
type_
IXMLDOMNode.nodeType
IXMLDOMDocument
IXMLDOMDocument.createNode
IXMLDOMSchemaCollection.add
IXMLElement
IXMLElement2
var
HTTP
: IXMLHTTPRequest;
XMLDoc : IXMLDomDocument;
...
...
XMLDoc := CoDOMDocument.Create;
HTTP
:= CoXMLHTTP.Create;
// Issue POST HTTP request to the URL.
with HTTP do begin
Open('POST', URL, False, '', '');
Send('');
// ResponseXML now holds resulting XML document.
XMLDoc.Load(ResponseXML);
end;
:=
:=
:=
:=
'http://terra/northwind?sql=';
'SELECT * FROM Employees';
SQL + XMLMethod;
URL + StringReplace(
SQL, ' ', '%20', [rfReplaceAll]);
Weve also used the XMLMethod variable, which species one of the
FOR XML modes:
FOR XML RAW
FOR XML AUTO
FOR XML AUTO, ELEMENTS
22 September 2001 Delphi Informant Magazine
XMLDoc := CoDOMDocument.Create;
// Load remote XML document asynchronously.
XMLDoc.Async := False;
XMLDoc.Load(URL);
Then take the root node of the XML document, and access its attributes node:
Root
:= XMLDoc.DocumentElement;
Attribs := Root.FirstChild.Attributes;
Next, extract the tables column names. To do so, iterate the Attribs
collection (of the IXMLDOMNamedNodeMap type) of the rst child
node of the XML document, and extract the names of each node in
this collection (see the rst for loop in Figure 7).
Now we can show the data. This is done with a similar technique, as
shown in the second for loop in Figure 7, but this time we iterate all
child nodes of the XML document. The result of our manipulations
is shown on another example form, containing a Button and ListView
component (see Figure 8).
To study the resulting XML representation for just one row of data, lets
add a Memo component to the form. Then enter the code shown in
Figure 9 for the OnClick event handler for the ListView component.
Now, when we run our example and click on a row, well receive its
XML representation, as shown in Figure 10.
After this the rst node is shown using the ShowOneNode procedure
(see Figure 13).
The code for the four SpeedButtons one for the rst row, the
previous row, the next row, and the last row is shown in Figure 14.
Conclusion
Thats it for this month. Weve seen how to extract data from Microsoft
SQL Server 2000 via its XML features. These features allow us to
extract data without using any data access components by directly
specifying our SQL query, and using the HTTP protocol to talk with
the SQL Server. We also saw several examples of how to represent the
extracted data in Delphi applications by working with the Microsoft
XML Document Object Model (DOM).
Next month, well examine data querying techniques for two more
FOR XML modes (AUTO and EXPLICIT), and demonstrate how to
use XML templates to separate our Delphi code from the XML-based
queries. See you then.
The projects referenced in this article are available on the Delphi Informant Magazine Complete Works CD located in INFORM\2001\SEP\
DI200109AF.
Alex Fedorov is a Chief Technology Officer for Netface SA, based in Lausanne,
Switzerland (http://www.netface.ch). He was one of the co-authors of Professional Active Server Pages 2.0 (Wrox, 1998) and ASP Programmers Reference (Wrox, 1998), as well as Advanced Delphi Developers Guide to ADO
(Wordware, 2000).
Kylix Tech
By Brian Burton
For example, suppose your application is called Foobler and is installed in the directory /usr/local/
Foobler. Further suppose that the application uses
several shared libraries that are all installed in the
Foobler directory. Any user that wants to run
Foobler will need to modify his or her shell init le
(.prole for bash or ksh, .login for csh) to dene
LD_LIBRARY_PATH with the Foobler directory. If
the users are using bash (the default shell for new
accounts on many Linux distributions) they will need
to add the following to their .prole le:
LD_LIBRARY_PATH=/usr/local/Foobler:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
Kylix Tech
const
APP_SUFFIX = '.bin';
procedure FindTarget(var target_name, target_dir: string);
var
my_name : string;
begin
my_name := ExpandFileName(ParamStr(0));
target_name := my_name + APP_SUFFIX;
target_dir := ExtractFileDir(my_name);
end;
relied mostly on routines from the System unit. However, launching the
application involves a call to a Linux system call located in the Kylix
Libc unit. The system call, execve, allows a program to recreate itself as
another program.
On UNIX-based systems such as Linux, every running process has a
parent process. Therefore, starting a new program generally involves
two system calls. First the fork system call is used to create a new child
process. The child created by fork is a nearly exact replica of the parent
process that created it. After the fork call, both the parent and child
processes have nearly identical memory images, and execution of each
process continues at the instruction immediately following the call to
fork. Usually the fork call is made in a conditional statement that allows
the parent to continue processing normally, while the child performs a
few housekeeping chores and then calls the execve system call.
The execve system allows a process to completely recreate itself as an
instance of a different program. When execve is called, the Linux kernel
identies the executable le to be executed and replaces the running process current memory image with one appropriate to the new program. In
a sense, the process is transformed from one program to another.
Theres no need to call fork in the case of the launcher program. The
launcher can simply transform itself into an instance of the application
once the environment is initialized.
The Libc unit provides a family of related procedures for invoking
the execve system call. Before calling execve, each procedure accepts
slightly different parameters and performs some special processing.
The launcher example uses the execv procedure, which accepts the
absolute path to the executable le to launch and the complete set of
command-line arguments for the program.
The Kylix System unit exposes the command-line arguments to the
application as the platform-specic global variables, ArgCount and
ArgValues. ArgCount contains the number of command-line arguments in ArgValues. ArgValues is a pointer to an array of PChars with
ArgValues+1 elements. The rst element of the array contains the path
to the executable le of the running program. The last element of the
Kylix Tech
var
target_name : string;
target_dir : string;
begin
FindTarget(target_name, target_dir);
SetPath('LD_LIBRARY_PATH', target_dir);
SetPath('PATH', target_dir);
ExecTarget(target_name);
{ We only reach this point if execv fails. }
Writeln(ErrOutput, 'error: unable to execute ' +
target_name);
Writeln(ErrOutput, Format(
'code=%d msg=%s', [errno, strerror(errno)]));
end.
array is a nil pointer to indicate the end of the array. The elements
in between (if any) are the additional command-line arguments used
to launch the program.
To launch the application, the sample launcher simply copies its own
ArgValues array (see Figure 3) and changes the rst element to refer
to the program being launched. Then it calls execv to actually launch
the program. Replacing the rst element in ArgValues isnt strictly
necessary, but by doing so the launched application can easily identify
its own executable le, and is completely oblivious to the fact that it
was started by the launcher, rather than the users shell.
Possible Improvements
The sample launcher could be extended in any number of ways to
make it more useful for a given application. For example, if the application stores its executable les in a bin directory, and its shared libraries
in a lib directory, the launcher could be modied to set the PATH
and LD_LIBRARY_PATH to different values, thus:
SetPath('LD_LIBRARY_PATH', target_dir + '../lib');
SetPath('PATH', target_dir);
The launcher could also be enhanced to set other environment variables needed by the application. For example, Kylix sets the variables
HHHOME and XPPATH in its kylixpath script.
The les referenced in this article are available on the Delphi
Informant Magazine Complete Works CD located in INFORM\2001\
SEP\DI200109BB.
Greater Delphi
COM / Delphi 3-6
By Alessandro Federici
Introduction to COM
The Basic Building Blocks of Windows Development
few months ago, while looking for my first house, I spent quite a lot of time
with a real estate agent. This taught me the three most important aspects of that
business: location, location, location. With the Component Object Model (COM), the
reverse is true; location is ideally the last thing in which you are interested. Instead,
the mantra is integration, integration, integration.
Every Windows user, whether aware of it or not,
deals with COM every day. COM is used by
Microsoft Ofce when we run the spell-check utility, by many Web sites running IIS, and by the
operating system for some of its mundane tasks.
Some developers choose COM specically to build
complex, scalable, and secure enterprise systems.
Integration Yesterday
In our eld, integration can be accomplished in
many ways, and it can be applied to many things.
Imagine you are developing a word processor application. You will create or inherit a custom memo
control for editing purposes, you may include a
spell checker, and if you want to get fancy
you may also want to include a set of custom
routines that allows your users to convert the docu-
This works ne and you will achieve your objective. GetConverters returns a custom TList (TCon-
Greater Delphi
To recap, here are some of the problems that COM solves:
Object-oriented language independence
Dynamic linking
Location independence
Figure 2: Delphi uses type library information to generate interfaces it can use.
verterList) which may have some Delphi methods that make it handy
and easy to use.
Unfortunately this isnt an optimal solution. Worse, it doesnt work unless
youre using Delphi or Borland C++Builder. To use the result of the
function GetConverters pointer as a TConverterList, the client needs to
know what a TConverterList is. To do this, you need to share that
information. Even if you do, however, youd have a problem with nonBorland compilers. TList is a VCL class. Its not included, for instance, in
Microsoft Visual C++ or Visual Basic; developers in those environments
couldnt benet from the pointer we return.
You could have structured your DLL differently, following, for example, the approach of the Windows API function, EnumWindows,
which takes a pointer to a callback routine. Another solution would
have been to export more functions. Whichever approach you may
choose, however, youd still be conned to a world of simple data
types that is everything but object-oriented. On top of that, the DLL
has to be run on the clients computer.
Integration Today
COM is one of the technologies that helps us resolve some of these
issues. COM has a long story. Ofcially, the acronym COM was rst
used around 1993. We can trace COM roots back to Windows 3.x
where DDE and OLE were used in Microsoft Word and Excel
as a sort of rudimentary communication and interoperability glue.
Today COM is everywhere on the Windows platform. Small applications such as ICQ, CuteFTP, or Allaire HomeSite, are accessible
through COM. Application suites such as Microsoft Ofce are based
on COM. Windows-based enterprise systems leverage COM and
Microsoft Transaction Server for business-critical operations. If you
develop on Windows, you will have to face COM sooner or later. The
sooner you do, the easier it will be.
This article is about understanding COM and the reasons its important, rather than providing another how-to tutorial. Well start with
the principles behind COM, then provide a concrete example you
can download and examine for yourself (see end of article for
details). The rst part wont take long, but will give you a better
understanding of what happens in the example and why.
29 September 2001 Delphi Informant Magazine
Dynamic Linking
Similar to DLLs, COM allows and actually only works through
dynamic linking. You can choose to take advantage of this in two ways:
early binding or late binding.
Before we continue, you need to register your COM library. Registration
is the process through which Windows becomes aware of a COM object
and learns how to instantiate it. To do this, you need to use a special tool
called REGSVR32.EXE (contained in Windows\System32), or Borlands
equivalent tregsvr.exe (in \Delphi5\Bin for example). Another way, if you
have the Delphi source code, is to open the COM project (in our case
COMConverter.dpr) and select Run | Register ActiveX Server.
Registering a COM server means inserting special keys into the
Windows registry. The information you will store includes the
name of the DLL or EXE le that hosts your COM object, the
identiers that uniquely identify it (see the yellow on green code
in Figure 2) and a few other items. If you dont do this, Windows
wont be able to instantiate your COM object.
Continuing our analogy to DLLs, early binding is similar to importing routines from a DLL by using the external directive. When you
Greater Delphi
do that, you embed the denition of those routines in your client,
and you expect them to match that denition exactly when you
connect to them at run time. If the name, parameters, or result type
are changed, youll have an error as soon as the application starts.
Late binding is similar to the GetProcAddress API call in which you
specify the name of the function you want to connect to using a
string, and are returned a pointer. When you do that, your client runs
ne, unless you try to use the function with the wrong parameters.
Invoking methods of a COM object through early binding is
faster than using late binding. Every time you use late binding,
youre asking Windows to look for a method called with a certain
name, return a pointer to it, and then invoke it. Using early
binding means you immediately call it, without any additional
overhead, because you already know the location of that methods
entry point. However, using late binding allows much more exibility, and makes possible things such as scripting.
As an example, this is the content of the VBTest.vbs le contained
in the example applications \WordProcessor directory:
Dim MyObj, i, s
Set MyObj = CreateObject("COMConverter.ConverterList")
s = ""
For i = 0 To (MyObj.Count-1)
s = s & MyObj.Items(i).Description & ", "
Next
MsgBox("You can save as " & s)
implementation
uses ComObj;
{ $R *.DFM }
procedure TForm1.bLateBindingClick(Sender: TObject);
var myobj: OleVariant;
begin
myobj := CreateOLEObject('COMCOnverter.ConverterList');
ShowMessage('There are ' + IntToStr(myobj.Count) +
' converters available');
end;
procedure TForm1.bEarlyBindingClick(Sender: TObject);
var
myobj : IConverterList;
begin
myobj := CoConverterList.Create;
ShowMessage('There are ' + IntToStr(myobj.Count) +
' converters available');
end;
Conclusion
The fact that you declared myobj as an OleVariant is the key here.
That tells Delphi how you invoke the methods of a COM object.
Any time you use an OleVariant you can specify any method name.
The compiler wont complain. Try putting myobj.XYZ in the rst event
handler. It will compile successfully, but at run time Delphi will raise
an exception as soon as you hit that line of code. In the second
case, you wouldnt be able to compile it, because IConverterList doesnt
dene a method named XYZ.
Location Independence
Not too many years ago the terms distributed and thin client
became very popular. The two terms are often used together when
discussing systems physically split into presentation, business, and data
storage tiers (multi-tier or three-tier systems). Physically split means
that each of those tiers can be running on the same machine, or
on separate ones. In brief, multi-tier architecture was conceived to
produce cleaner designs and enhance scalability. However, its a topic
unto itself and outside the scope of this article.
30 September 2001 Delphi Informant Magazine
If you werent familiar with COM, I hope this article provided some
interesting information to get you started. If you are already using
COM, I hope it helped you understand a little better why COM
exists and when you can benet from using it.
Greater Delphi
The demonstration applications referenced in this article are available
on the Delphi Informant Magazine Complete Works CD located in
INFORM\2001\SEP\DI200109CM.
Resources
If you want to read more, I recommend the following introductory books:
Understanding COM+ by David S. Platt, ISBN: 0-7356-0666-8,
Microsoft Press, 1999
Inside COM (Programming Series) by Dale Rogerson, ISBN:
1-57231-349-8, Microsoft Press, 1996
COM and DCOM: Microsofts Vision for Distributed Objects by
Roger Sessions, ISBN: 0-471-19381-X, John Wiley & Sons, 1997
You can also nd more information online at:
Microsofts COM pages at http://www.microsoft.com/com
Binh Lys Web site at http://www.techvanguards.com
Dan Misers Distribucon site at http://www.distribucon.com
Deborah Pates rudimentary home page at http://
www.djpate.freeserve.co.uk
Originally from Milan, Alessandro Federici is a recent Chicago transplant. Hes been
developing software since he can remember and has used Borland tools since
Turbo Pascal 3.02. Hes spent the last four years working extensively with the
technologies included under the Microsoft Windows DNA umbrella (COM, DCOM,
COM+, MTS, MSMQ, ASP, IISS, SQL Server, etc.), and developing distributed
systems with InterBase and Oracle as well. Hes worked as a consultant and
has been the Localization Software Engineer on a number of Microsoft projects
(Milan and Dublin). In the US, hes worked as a developer and system architect
for companies in the financial, warehousing, and supply chain management businesses. He is a system architect and team leader for the development division of
eCubix. You can contact Alex at alef@bigfoot.com, or through his Web site at
http://www.msdelphi.com.
Sound+Vision
DirectX / DirectDraw / Bitmaps / Delphi 5
By Brian Phillips
DirectDraw Delphi
Strategies for Working with Large Bitmaps
Background
One of the more critical limitations to DirectDraw
involves the use of large bitmaps as image sources.
Bitmap sizes play a big role in whether DirectDraw
will function at all for certain applications. A
DirectDraw surface wont be created if it violates
the size constraints enforced by DirectX. While
displaying large bitmaps is usually fairly easy, doing
Sample Code
Figure 1: This satellite image of the United States is simply too big for DirectDraw.
The sample code included here used a 65MB satellite picture of the United States (see Figure 1)
as a test image. DirectDraw refused to create a surface this size. This discussion is about how to load
and display these large images without running into
DirectDraw roadblocks and do it in a way that
preserves the smooth navigation of the source image
that large images inherently need.
Sound+Vision
TQuadSurface = class(TQuadSurfaceFace)
private
NE, NW, SE, SW: TQuadSurface;
bounds: TRect;
procedure SetUpPic(var r: TRect; b: TBitmap;
lpdd: IDIRECTDRAW7);
function GetSurface(var r: TRect; lpdd: IDIRECTDRAW7;
b: TBitmap): IDirectDrawSurface7;
protected
surface: IDirectDrawSurface7;
public
procedure DrawOnSurfaceRect(var frontArea: TRect;
backArea: TRect; target: IDirectDrawSurface7);
override;
function getSurfaceRect: TRect;
function getWidth: Integer; override;
function getHeight: Integer; override;
procedure getRect(var r: TRect); override;
function Init(lpdd: IDIRECTDRAW7; File_Name: string):
Boolean; override;
function InitWithZoom(lpdd: IDIRECTDRAW7;
File_Name: string; Scale: Double): Boolean;
overload;
function InitWithZoom(lpdd: IDIRECTDRAW7; map: TBitmap;
Scale: Double): Boolean; overload;
constructor Create; virtual;
destructor Destroy; override;
end;
Double Buffering
Smooth, icker-free image display is achieved by using double buffering.
Double buffering is performing all drawing actions on a back surface,
then using a single fast copy to move the image from the back buffer
to the screen. Since the screen isnt required to update when every draw
command goes through, its signicantly smoother to the eye.
Most DirectDraw enthusiasts will recognize the ip function as double
buffering. In this example code, I avoid using ip in order to show a more
generic version that can work outside DirectX. Also, most scientic and
business applications dont use full screen mode, since word processors,
math packages, etc. are usually being used alongside graphic displays.
Culling
Since the smoothness of graphic display is a function of the amount of
work done between paints, the workload must be intelligently managed.
Work that isnt required for a specic screen shot needs to be culled.
The rule here is simple: dont do any work that isnt necessary. This can
be much more challenging than simply fullling all graphics work that
needs to be done, but leads directly to increased speed and efciency.
The sample code accomplishes this by using a quad-tree structure. Each
branch of the quad-tree understands how to cull itself, and to nd lower
33 September 2001 Delphi Informant Magazine
branches that t within the drawing zone. Some would argue that using
a quad-tree presents extra work, and that direct-array access is more
efcient. However, the quad-tree structure offers good performance and
is easy to understand in sample code, so its used here.
Sound+Vision
Example Form
ROOT
NE
SE
SW
NW
NW
NE
NW
NE
NW
NE
NW
NE
SE
SW
SE
SW
SE
SW
SE
SW
Navigation
Once a large bitmap is loaded into memory, it still must be displayed to the screen. Since the bitmap is fairly large, a direct copy
to the screen would need excessive processor time. DirectDraw
surfaces support culling, and letting DirectDraw use its native
clipping boundaries would work, but there are easy ways to cull
out unneeded pixel transfers a bit faster.
By nding the intersections of the area that will be displayed on the
screen with the new, small bitmap regions, the proper areas of the
DirectDraw surfaces can be dened and copied onto the back buffer. The
search within the quad-tree structure will be fairly quick, and eliminate a
full search of all the possible rectangles.
To ll the back buffer with the proper subsection of the image, two
rectangles are needed. The rst denes the coordinates of the screen area
where the image will be displayed. The back buffer needs to be at least
large enough to encompass that area. Making the back buffer equal in
size to the entire screen area is an easy solution to this.
The second rectangle is the area to sample from the original
large bitmap. Dening a focus point (usually x and y) within the
bounded area of the original image is an easy way to keep track of
where the view is located. As long as the focus is conned to the
bitmap, a visible transfer will occur.
There will be occasions when the focus will reveal the edges of the
bitmap. In those situations it may be prudent to dene a perimeter
zone surrounding the bitmap where the focus wont be allowed. This
zone (usually half of the display area width on each side, and half
the display area height for top and bottom) will stop corners from
overlapping on the screen.
During the sampling operations, its important to remember that
the original bitmap that was sampled is no longer in memory.
Instead, it has been broken into smaller pieces and copied onto
individual DirectDraw surfaces. The pieces in each leaf of the
containing tree still have the bounds property that was assigned
during the split, which dened their original coordinates within
the original image. These bounds rectangles will be used to sample
the leaf surfaces and create the back-buffered image.
34 September 2001 Delphi Informant Magazine
Sound+Vision
angle. Also, each node has a bounds property that represents the
rectangle from which the node was sampled in the original bitmap
image. Leaf and branch nodes all have bounds properties.
TQuadSurface also has a reference to an IDirectDrawSurface interface.
This interface isnt created except at the leaf nodes, where it holds the
small panels that make up the entire image. Creating the TQuadSurface
initiates all of the surface references to nil. No image work is actually
performed until its Init method (shown in Figure 5) is called.
The form unit calls Init to set up the bitmap into memory. It
accepts two parameters: a DirectDraw interface capable of creating
surfaces, and the bitmap le name to load into the tree. The bounds
properties becomes equal to the area of the bitmap being loaded.
Once the root node bounds properties are set, the recursion is ready
to begin. SetUpPic is called from Init to recursively construct the
tree. The unneeded source image is freed when nished.
SetUpPic accepts a TRect area initially equal to the bounds of the
source bitmap, and a reference to the IDirectDraw passed into
Init. The recursion begins by checking if the rectangular area is
small enough to satisfy the maximum size acceptable (the sample
code uses a 256x256 pixel size, but it could improve performance
to dene a larger area). If the area is small enough to fulll the
needed maximum size, GetSurface is called to instantiate a reference to a DirectDraw surface. The recursion then stops for that
leaf, and returns control to the calling object.
If the rectangle is larger than the maximum size needed to create
a DirectDraw surface, it must be broken into four parts. The
midpoints of the rectangle are found, along with rounding information. Rounding operations are crucial in these types of applications; they can generate strange lines or offsets within the bitmap.
The easy solution used here is to let the rectangles overlap by a
single pixel.
Four new TQuadSurface objects are created from the area. Each
calls its own SetUpPic with the four new areas generated by the
bounds of our new regions. This continues until the subdivided
rectangles are within the bounds of the maximum size, where they
call GetSurface and return to their calling object.
The trick comes when projecting that point onto another line. It
can be done easily by multiplying the ratio across the new basis and
adding any offset. For example: suppose the value of 20 on line 10
to 30 needs to be transformed to a line that starts at value 100
and ends at value 200. In order that the point 20 (from 10 to 30)
be transformed so its new line is from 100 to 200, the same ratio
operation takes place that took place previously.
Once the intersection area of the leaf node is found, the area that needs
to be copied from the leaf nodes surface onto the back buffer must
This applies directly to what DrawOnSurfaceRect does. An intersection rectangle is found from the bounds of the leaf node and the
Sound+Vision
back area that must be lled. The frontArea variable represents the
transformation of the backArea onto the screen. Since they have the
same width-to-height ratio, a transformation along both axes will
appear to zoom in or out, but not skew. Therefore, the coordinates
found in reference to the backArea can be transformed using the
above math into coordinates useful for the screen rendering from the
back buffer. For claritys sake, the example code does this without
trigonometric functions.
The DirectDrawSurface can now copy bits from the intersected
area (in its own surface area, not the bounds area) onto the back
buffer area dened by the transformed intersection rectangle. Once
the data transfer is nished, it is up to the function that called
DrawOnSurfaceRect to determine what to do with the back buffer
that has been lled. Usually it is simply ipped or blitted onto the
primary surface for display.
Other Issues
There are a few issues to consider when using this technique. The size
of the bitmap must be of a proper ratio. For instance, if the bitmap
were two bytes wide and 10000 bytes tall, dividing it up into quads
would cause a problem there would be zero sized branches and
leaves. A good rule of thumb would be to not let the smaller size be
less than the log2 of the larger size.
Zooming is also an issue. The TQuadSurface supports zooming by
changing the size ratios of the input screen and back buffer sizes. This
method becomes inefcient as the amount of information transferred
to the back buffer increases. Noticeable lags can occur in operations
as zoom scales reach 1/4 or less. This is just because the amount of
work needed is growing as the zoom out occurs.
There are ways to get around this. The usual method is to shrink
the original bitmap to a more manageable level and rebuild the
tree. Or, if the system isnt memory-constrained, a better solution
might be to have three or more TQuadSurface objects, each one for
a different level of detail, and a broker object that understands how
to change the back rectangle needed into the proper operations in
the correct TQuadSurface.
Conclusion
By breaking up an image into smaller parts, the limitations of the
DirectDraw surface size can be overcome quickly. When reassembling
the image, special care must be taken to adequately render the proper
sections of several of the new smaller sub-images correctly. Rounding
errors can occur, and the use of ratios can result in a zoom capability.
By confronting the image management problem, DirectDraw
becomes a very usable feature for processing and displaying large
images. These techniques arent only usable for DirectDraw, but for
any two-dimensional raster image processing.
The demonstration project referenced in this article is available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
2001\SEP\DI200109BP.
ExpressBars Suite 4
The New Look with Much Less Work
ay you need to write a Windows application. You know, the old-fashioned kind.
Not a Web server, not a MIDAS server in the logic layer of an n-tier application,
nothing exotic and exciting like wireless services, just good old-fashioned windows,
menus, dialog boxes, and the like.
Ah! thats easy you say. Let me show you how I
create a new form here, drop a couple of controls,
add a menu component, throw in a panel, align
it to the top so it will be a toolbar, add a status
bar, and were ready to rock and roll.
A menu-bar component set. Who needs it?
Delphi already ships with MainMenu and PopupMenu components. Theres a StatusBar component for well the status bar, ToolBar and
CoolBar for the toolbars, or the granddaddy of
mentation. I used it for the minimal functionality in a new application I was writing, yet I nd myself using it again and again.
The Components
ExpressBars installs 11 new components into the Component palette on a new ExpressBars tab. Three of these components are used
for the SideBar: an Outlook-like component with groups and items
that appear at the left side of an applications window. TdxSideBar
is the main Outlook bar-like component. Drop it on your form,
double-click to activate the component editor, and start adding
groups and items from there. For each group, you can dene the
visibility (groups might be activated based on user authorization
for example), the size of the icons for the items it contains, its
position compared to that of other groups, and its name. For each
item, you can assign the icon, a tag, a hint, its position within the
group, and custom data.
Once your groups and items are dened, its easy to use the
OnItemClick event to change views based on the item selected. The
component allows you more ne-grained control to allow you to
respond to events that happen when the active group changes, the
item selection changes, before or after a user edits an item, etc.
If you want to offer user customization of the SideBar, so they
can enable or disable groups, add items, etc., youll need to use
the TdxSideBarStore and assign the SideBars Store property to
point to it. Finally, the TdxSideBarPopupMenu can be assigned
to the SideBar component to provide standard customization
operations popup menus such as Add Group, Remove Group, Rename
Group, Remove Item, etc.
Now lets move on to the real power of this product: the menu and
toolbar components. To start working on your menu and toolbar
design, drop a TdxBarManager on your form and double-click on
it. Just as in Ofce, a toolbar and a menu are pretty much the
same. Dene toolbars in the component editors Toolbars tab. You
can designate a toolbar as the main menu of the form to have it
automatically appear at the top of your form looking like a regular
menu, or you can leave it as a regular toolbar that will appear as a
docked toolbar or a oating command window.
38 September 2001 Delphi Informant Magazine
If you cant nd the item you want to place on your toolbar among
the different classes offered, you can create a control container
command and assign an element of your design. Link this command object to the object that you place on your form, with its
visibility turned off, and youre ready to go. One of the samples
that come with ExpressBars shows how to include a Delphi-like
component palette on a toolbar.
Every command item can be customized with an associated image,
a title that can be displayed with it, and for the different
command types a set of properties and events that allow you
to customize the use of this command. Since there are 21 built-in
command types, we wont go into the details. Instead, lets return
to the toolbars.
Once you dene a toolbar, you can dene if it will be docked to
one of the four sides of the form. The bar manager will, of course,
allow you to customize to which side, if any, the toolbar can be
docked. You can also determine if users can let it oat. Then you
can start dragging and dropping command objects on it to dene
its appearance. The commands will automatically be added and
aligned, so you wont need to ght with the placement of the
elements the way you would if you were to use a simple TPanel
for a toolbar.
When a toolbar is selected in the component editor, you can assign
a plethora of options to it, such as the location where it is docked
and to which sides of the form it cant be docked. You can also
dene if the toolbar shows a size grip, if it takes the entire row
(dock width), if it can span multiple lines, what its caption is when
it oats, where it rst appears when it is oating, what kind of
border it has, and what kind of customization it provides.
Customization is a big feature offered by this product.
The samples that ship with the product are also a good source
of information and can teach you a lot. On top of this, the
newsgroups server provides peer support and is frequently manned
by Developer Express employees, so you will usually get excellent
technical support and answers to your questions.
Conclusion
Theres also a happy middle ground. The Help les that ship with
the product are integrated with Delphis Help system upon installation for a complete and helpful reference. However, Developer
Express took the time to write a couple of tutorial-style articles
(http://www.devexpress.com/soapbox.asp) that can get you started
in a hurry. These do a good job of explaining some of the capabilities, thus steering you to explore on your own once you understand
what features can be achieved with the different components.
QL Tester 1.2 from Red Brook Software, Inc. lets you build and test SQL statements
without having to compile and run your application. This architecture lets you
use fewer query components, as well as store all your SQL statements outside your
application, so they can be changed without recompiling your program.
SQL Tester is particularly useful if youre creating
an application and you want to store your SQL
statements in external les and execute them by
using the LoadFromFile method of one of the
Delphi query components. Figure 1 shows the
main SQL Tester form.
Getting Started
SQL Tester lets you create SQL statements using
any database you can access via the BDE or ADO.
popup menu, then enter the alias in the dialog box and click OK.
You only need to do this one time because the table aliases are saved
in SQL Testers database.
If your query contains parameters, simply click the Parameters
button at the bottom of the main form to display the dialog box
shown in Figure 2. Use this dialog box to enter parameter values for
each of the parameters in your query. This allows you to test your
SQL statement with different parameter values.
After youve entered your SQL statement and assigned values to parameters, you can run the SQL statement by clicking the Run button at the
bottom of the main form, or by clicking the Run speed button to the
left of the SQL Statement memo control. If you run a SELECT statement,
the result set appears in the Query Results grid at the bottom of the main
form. Optionally, you can have the query results displayed in a separate
window. This lets you have up to four result sets open at one time.
You can export the query result set in either ASCII or XML format.
You can also request a live result set when testing queries, so you
can update the result set to modify your test data without having
to use another program.
If youre working with a long SQL statement, the SQL Statement
memo control may not be large enough to allow you to see the entire
SQL Tester lets you build and test SQL statements without having
to compile and run your applications. It can decrease your development time with its ability to add table names and field names
to SQL statements using drag-and-drop, and you can batch test
queries when the database schema changes. SQL Tester can also
read SQL statements from Delphi .dfm files.
Red Brook Software, Inc.
554 Watervliet Shaker Road
Latham, NY 12110
Phone: (518) 786-3199
Fax: (518) 786-3511
E-Mail: sales@redbrooksoftware.com
Web Site: http://www.redbrooksoftware.com
Price: SQL Tester 1.2, US$195.00
statement. To solve this problem, click the second speed button to the
left of the SQL Statement memo control and the dialog box shown in
Figure 3 will be displayed, containing the entire SQL statement. This
dialog box is resizable and provides a much larger working area.
Putting It to Use
After youve created and tested your SQL statement, youll want to
move to the Identification group (again, see Figure 1). Here you can
enter a Description for the statement. You can also enter a Group Code
to identify the SQL statements by the project to which they belong.
Use the Group Code edit box to select SQL statements for certain
batch operations. If the Database edit box doesnt contain a database
name, simply drag the database name from the Databases list box. If
you want to save the SQL statement to a le, enter the name in the
File Name edit box, and press CS (or choose File | Save).
Another nice feature is that SQL Tester lets you save your current
SQL statement as a template if you need to create many similar
queries. Click the Insert button on the navigator bar to create a query
from the template, then click the Load from Template button to display
the list of templates youve created. Double-click the template you
want to use and the SQL statement will be inserted into the SQL
Statement memo control at the insertion point. Templates dont have
to be complete working SQL statements. For example, you might
save a list of eld names you want to use in many queries.
Another timesaving feature is the Duplicate button. View any
query in the database, click the Duplicate button, and SQL Tester
creates a new query with the same SQL statement and the same
identication information.
SQL Tester lets you print queries two ways. You can print the
current query by clicking the Print speed button to the left of the
SQL Statement memo control. Otherwise, clicking the Print button
on the toolbar lets you print all the queries within a statement
number range, all queries with the same group code, or all queries
for the database you choose.
Another practical feature of SQL Tester is the ability to batch test
queries. This is handy if you make changes to your database and
want an easy way to determine which queries are affected by the
changes. To test a batch of queries, choose Tools | Batch Testing.
The Batch Testing dialog box lets you select the SQL statements
to test, and offers three ways to test them: You can choose to
Conclusion
SQL Tester can cut your development time in two ways. First,
the ability to add table names and eld names to SQL statements
using drag-and-drop can save a lot of typing when youre creating
queries. Second, you can batch test queries when the database
schema changes. Batch testing all queries in an application using
SQL Tester is a lot faster than opening every query component
in your application in the Delphi IDE to see if the query still
executes properly with a new database structure.
TextFile
TextFile
ISBN: 1-55622-736-1
Cover Price: US$59.95
(525 pages, CD-ROM)
http://www.wordware.com
ISBN: 0-07-212947-6
Cover Price: US$59.99
(832 pages)
http://www.osborne.com
ISBN: 0-471-14965-9
Cover Price: US$64.99
(276 pages)
http://www.wiley.com
TextFile
SQL in a Nutshell
UML Explained
There are many books about
the Unied Modeling Language. Do we really need
another? Most of the current
crop were designed for readers
with considerable technical
expertise, but theres a need
to present UML in a manner
accessible to those with whom
engineers must communicate
(stakeholders). Kendall Scotts
UML Explained is aimed especially at such stakeholders, and
lls this important gap.
UML Explained
by Kendall Scott,
Addison-Wesley,
http://www.awl.com.
SQL in a Nutshell
by Kevin Kline with Daniel Kline, Ph.D.,
OReilly Computer Books,
http://www.oreilly.com.
ISBN: 0-201-721820-1
Cover Price: US$29.95
(151 pages)
ISBN: 1-56592-744-3
Cover Price: US$29.95
(214 pages)
The Elements of
User Interface Design
Theo Mandel
John Wiley & Sons, Inc.
ISBN: 0-471-16267-1
Cover Price: US$49.99
(440 pages)
http://www.wiley.com
ISBN: 0-471-36366-9
Cover Price: US$49.99
(460 pages)
http://www.wiley.com
ISBN: 0-07-212739-2
Cover Price: US$49.99
(793 pages)
http://www.osborne.com
Best Practices
Directions / Commentary
o matter how intuitive you feel your application is, there will be times when its users, or at least some of them,
will be confused. Theyll need answers to questions that arise as to how to use the program, or guidance on
how to accomplish a particular task. Its customary to provide a separate Help file for just such occasions. However,
sometimes that may seem like overkill, or you may feel its a waste of time to produce one as nobody ever
reads the Help documentation anyway. Moreover, its YAF (yet another file) that needs to be deployed with your
application, and another tool you have to learn. Or you can hire a contractor to create it.
Nevertheless, you should provide help in some form for users. You
have three choices:
Provide no help whatsoever. Bad idea.
Provide a full-edged Windows Help le.
Provide a quick-and-dirty pseudo Help system.
If youre serious about offering a quality software package, the rst
option is obviously not a good one, so you must ask yourself:
Do I write a conventional Help le or not? If you do, you
can use a tool such as RoboHelp (http://www.ehelp.com), Doc-ToHelp (http://www.wextech.com), Microsoft Help Workshop, which
comes with Delphi (\Program Files\Borland\Delphi X\Help\Tools),
or HelpScribble (http://www.jgsoft.com/helpscr.html), which is written in Delphi.
If you do opt to create a conventional Help le, when is the best
time? That is, will you write the Help le before writing the application, or wait until afterward? Many software methodology experts
recommend writing the Help le rst, basing it on the detailed
design specications. An advantage to that approach is coders can
then use the Help le as the product specication document. It
should be very clear how things are supposed to look and feel,
and just exactly what it is that the application does and doesnt do.
Preferably the le is complete, and contains mocked-up screen shots
of the forms. Adhering as much as possible to what is in the Help
le aids in preventing feature creep and gold plating.
Regardless of these considerations, you may decide to either postpone
the creation of the Help le until after the application is at least
partially complete, or eschew a separate Help le altogether. Thats
where the third option comes in, providing a poor mans Help system.
This technique is especially applicable for prototypes and utilities.
With a modicum of extra effort, you can provide just enough help
in a manner thats not tied to the Windows OS (which is especially
Best Practices
const
crlf = #13#10;
resourcestring
SGettysburgAddress1 =
'Four score and seven years ago our fathers brought,'+crlf+
'forth upon this continent, a new nation, conceived'+crlf+
'in Liberty, and dedicated to the proposition that'+crlf+
'all men are created equal.'+crlf+crlf+
'Now we are engaged in a great civil war, testing'+crlf+
'whether that nation, or any nation, so conceived,+crlf+
'and so dedicated, can long endure. We are met here'+crlf+
'on a great battle-field of that war. We have come'+crlf+
'to dedicate a portion of it as final resting place'+crlf+
'for those who here gave their lives that the nation'+crlf+
'might live. It is altogether fitting and proper'+crlf+
'that we should do this.'+crlf+crlf+
'But in a larger sense we can not dedicate--we'+crlf+
'can not consecratewe can not hallow this ground.'+crlf+
'The brave men, living and dead, who struggled here,'+crlf+
'have consecrated it far above our poor power to add'+crlf+
'or detract. The world will little note, nor long'+crlf+
'remember, what we say here, but can never forget'+crlf+
'what they did here. It is for us, the living,'+crlf+
'rather to be dedicated here to the unfinished work'+crlf+
'which they have, thus far, so nobly carried on.';
SGettysburgAddress2 =
'It is rather for us to be here dedicated to the'+crlf+
'great task remaining before us--that from these'+crlf+
'honored dead we take increased devotion to that'+crlf+
'cause for which they here gave the last full'+crlf+
'measure of devotion--that we here highly resolve'+crlf+
'that these dead shall not have died in vain; that'+crlf+
'this nation shall have a new birth of freedom, and'+crlf+
'that this government of the people, by the people,'+crlf+
'for the people, shall not perish from the earth.';
For code readability, add some constants that will correspond to the
topic you want to display, mapped to the HelpContext you provide
for the forms or controls:
const
Abraham = 0;
Martin = 1;
John
= 2;
File | New
Directions / Commentary
erious issues face us every day. How can I ensure my software is of the highest quality? What new technologies
should I learn this year? Will Delphi and/or Borland survive? Should I get involved with Linux/Kylix now or later?
Should I leave my company and become an independent consultant? Should I leave consulting and join a successful
company? This month we are going to take a break from such serious issues, and see how humor can help us keep
our cool in the face of incredible stupidity in the workplace.
As many of you, I subscribe to a number of Delphi Internet discussion lists and have learned much from reading their messages.
In the January 2000 issue of Delphi Informant I wrote a column
entitled The Case for Delphi in which I shared information from
two discussion threads explaining Delphis strengths compared to
those of Visual Basic. Here I will share a couple of recent discussions from two of my favorite lists: The Delphi Advocacy Group
mailing list (tdag@yahoogroups.com) and the main Project JEDI
list (Delphi-JEDI@yahoogroups.com). Discussions on TDAG are
sometimes technical in nature, but often concern themselves with
the well-being of Delphi and the company that produces it. The
JEDI list typically discusses the various activities of that Project,
but also include some excellent discussions of translation issues from
C/C++ to Delphi. The two threads I will present here, however, are
unusual and non-technical, but I think quite entertaining.
No more Delphi? No, we arent talking about the demise of our
favorite development tool, but rather about the precarious position of
Delphi in some companies. While replacing Delphi with Visual Basic
in a particular shop isnt farfetched, this short thread that appeared on
TDAG does stretch the limits of credulity. Some well-known Delphi
people participate on this list so Ive changed the name of one well
known tool to MyCoolTool to protect the innocent.
The discussion started with this post: Some of you will laugh,
thinking this is a joke. Some of you wont believe me at all. At the
government agency where I work, in our PC Team meeting last
Friday, it was announced that we were now going to begin converting
all of our Delphi programs into WordPerfect macros. The reason?
Make things simple so anyone can understand them. All the lawyers
and 25-year secretarial types gure they can write macros. So now the
big project is to eliminate EXEs altogether, and run the entire agency
under a (nearly obsolete) word processing program. The associate
programmer who works with me told them it was a good idea.
Theres nothing like government work!
The rst reply embodies the wonderful spirit of the developers on
this list, who would rather ght than switch: Just when you thought
you heard it all. Stupidity has reached new levels. The Associate
programmer you work with should be clubbed.
48 September 2001 Delphi Informant Magazine
File | New
We will repeat on the RAD market what Microsoft attained on
the browser market, said Ted Shelton, Senior Vice President, Chief
Strategy Ofcer, during a press conference which will be held early
tomorrow.
It didnt take long for the replies to start ying. The rst was an
expected one: This is great news! Have you forwarded it to the .nontech news group? However, a skeptic immediately contemplated: I
wonder if this policy will still be in effect on April 2! Surprisingly,
not everyone was pleased: A preposterous resolution I loathe! Fullers
open-sourcing obviates lucrative sales. A Borland stockholder, no
doubt. Well, you cant please everyone. Finally, someone asked the
obvious question, <grin> Do you have a URL for this story? To
which the Prime Perpetrator [PP] replied Aaaah, I knew I forgot
something :o] Then another doubting inquiry, Is this for real? I
plan to send this information to many people!!! To which the PP
responded, Sorry for the irritation. In some countries April 1 is a
day to make jokes and to pull someones leg. It is kind of tradition
to nd credible news which in reality is not true. As I said already,
this was just a joke :o]
Not everyone appreciated the discussion, so we had the request:
Please kill this thread, it is of no use and a bad joke as well. The
PPs defenders were waiting in the wings: I think it was a brilliant
joke: [IMHO it is a good idea to check something like this at the
originator: Is it that difcult to quickly check on the Borland site?
Another made the observation that is occurring right now to so many
of you: Always interesting how easy it is to fool people :-)
If you doubt that, then read one of the nal contributions: You
should see their faces! My team has gone crazy here. They really
believe the stuff. Some of them are already searching borland.com
for information. Others are forwarding it to their friends around
the world! Its a pity I dont have a camera here. (Hope I dont get
red!) I will tell them the truth tomorrow. The reason for this belief
is that Delphi in India is not used widely and these guys and gals
want Delphi to come up because they know that they will be hot
cakes if it comes up. Wish that Borland would really wake up with
its marketing!
Going back to the previous defense, that individual closed with
this recommendation for that special April day: Have a look at
slashdot.org. Its full of jokes today. Well I didnt, but you can bet its
on my itinerary for April 2002. Until next month...
Alan C. Moore, Ph.D.
Alan Moore is Professor of Music at Kentucky State University, teaching music theory
and humanities; he was named Distinguished Professor for 2001-2002. He has been
developing education-related applications with the Borland languages for more than
15 years. He is the author of The Tomes 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 has also published a number of articles in
various technical journals. Using Delphi, he specializes in writing custom components
and implementing multimedia capabilities in applications, particularly sound and
music. You can reach Alan on the Internet at acmdoc@aol.com.