Vous êtes sur la page 1sur 45

May 2001, Volume 7, Number 5

Cover Art By: Arthur Dugoni

ON THE COVER
5

On the Net

Writing Web Apps Dr Mark Brittingham


Server side, client side, ASP, PHP, CGI, ISAPI, HTML, VBScript
the list goes on and on. Whats a Web developer to do? Dr Mark
Brittingham sorts out the available technologies to recommend the
best approach for Delphi programmers. And guess what? Youre in
a good position.

FEATURES
10

The API Calls

Managing Windows Services Bill Todd


Delphi already makes it easy to create services for Windows NT/2000.
Now Bill Todd provides us with all the components and know-how we
need to make it easy to manage them as well.

30

Sound+Vision

Getting Started with OpenGL: Part I Eli Bar-Yosef


Eli Bar-Yosef begins a two-part series to introduce the world of
OpenGL programming to Delphi developers. This month he covers the
basics, from vertices and matrices to color and transformations.

34

At Your Fingertips

Kylix Form Tricks Bruno Sonnino


Youve seen how to do these tricks in Delphi, i.e. for Windows.
Now Bruno Sonnino demonstrates their Kylix counterparts. Theres a
difference, however: these tricks are of the cross-platform variety.

REVIEWS
38

XTNDConnect RPM
Product Review by Warren Rachele

19

Distributed Delphi

Creating MTS/COM+ Components Malcolm Matthews


From error handling to debugging, installation, and beyond, Malcolm
Matthews shows us how to write MTS components for Windows NT 4,
and the new COM+ components for Windows 2000.

41

25

2
43
44

Greater Delphi

Delphi, Excel, and OLAP Alex Fedorov and Natalia Elmanova


Alex Fedorov and Natalia Elmanova demonstrate how to implement
OLAP in Delphi applications, using advanced features of the Microsoft
PivotTable Services available through Excel Automation.
1 May 2001 Delphi Informant Magazine

List & Label Version 7.0


Product Review by Andrew Ghinaudo

DEPARTMENTS
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

Book Picks
Learning WML & WMLScript
Martin Frost
OReilly & Associates

AutomatedQA Announces AQtest


AutomatedQA has released
AQtest, an industrial-grade software test automation and management tool for Windows.
AQtest offers native support
for Borlands Delphi and
C++Builder, as well as for Visual
C++, Visual Basic, .NET and
Java. It also supports a variety
of development frameworks and
APIs, including VCL, ActiveX,
MFC, ATL, WTL, WinForms,
Swing and WFC, Win32, BDE,
and ADO.
AQtest offers support for
regression testing, as well as for
white-box testing. AQtest allows
private properties, methods, or
members to be controlled from
external scripts.
Scripts can be recorded, written,
edited, and executed in DelphiScript, VBScript, or JScript, and
can also be recorded or edited
in C++. Any application may be

compiled to use AQtest as an


OLE server. In that configuration, all tests can be run from
application code rather than
script code. Recorded scripts may
be called from the application or
integrated into source code.

AutomatedQA
Price: US$349 for a single license; fiveuser license, US$1,399; 10-user license,
US$2,499.
Contact: (702) 262-0609
Web Site: http://www.automatedqa.com/
products/aqtest.asp

TurboPower Announces Four Toolkits for Linux Development

ISBN: 1-56592-947-0
Cover Price: US$34.95
(179 pages)

Scripting XML and WMI for


Microsoft SQL Server 2000
Tobias Martinsson
John Wiley & Sons

ISBN: 0-471-39951-5
Cover Price: US$44.99
(413 pages, CD-ROM)

2 May 2001 Delphi Informant Magazine

TurboPower Software Co.


announced four new toolkits targeting programmers using Borlands Kylix for the Linux
development arena: Async Professional CLX, SysTools for
Linux, LockBox 2, and XMLPartner Standard Edition.
Async Professional CLX. This
version of TurboPowers serial
communications tool includes
com port control, high-speed
file transfer capabilities, and
VT100 terminal emulation. It
also includes TurboPowers Data
Pocket component, which raises
an event when programmerspecific data patterns are encountered in the incoming data stream.
SysTools for Linux. This collection includes routines for string
manipulation, date and time
math, regular expression and
mathematical expression evaluation, reusable container classes,
sorting, financial and statistical
routines, bar coding, and more.
LockBox 2. This updated
version of TurboPowers encryption library includes support for
public and private key encryption, digital signature support,
and more. LockBox 2 is a crossplatform library that works with
Kylix, Delphi, and C++Builder.

XMLPartner Standard Edition. Also a cross-platform


library that works with Kylix,
Delphi, and C++Builder, the
XMLPartner Standard components read and write XML data,
and have support for the XQL
query language.
These four products have
been written in optimized
Kylix source code (source code
included with each product),

and include documentation


and samples. They will be
available 45 days after Borland
ships Kylix.
TurboPower Software Co.
Price: Async Professional CLX, US$249;
SysTools for Linux, US$249; LockBox 2,
US$349; and XMLPartner Standard Edition,
US$199.
Contact: (800) 333-4160
Web Site: http://www.turbopower.com

Softel vdm Releases SftMask/ATL 4.0


Softel vdm, Inc. announced
the release of its ActiveX control, SftMask/ATL 4.0. This
masked edit control offers data
input and validation features.
Included are date and time
input fields, numeric integer
input, currency input, and
user-definable input masks.
Features include spin buttons,
user-definable minimum/
maximum range, and a pop-up
calendar.
SftMask/ATL has a built-in
caption, which can be aligned
with the edit control. It also
features auto-advance to the
next control on a form when
input has been validated.
SftMask/ATL also offers OLE
drag-and-drop support, and

employing ADO the control


can be used in data-bound
mode.
SftMask/ATL works in
Delphi, C++Builder, Visual
Basic, and other languages,
and supports Windows 95/98/
NT/2000. SftMask/ATL works
with Internet Explorer, so it
can be used directly in Web
pages.
The controls in SftMask/ATL
are royalty-free. The product
includes documentation, technical support, and free downloadable maintenance updates.
Softel vdm, Inc.
Price: US$199
Contact: softelvdm@softelvdm.com
Web Site: http://www.softelvdm.com

Delphi
T O O L S
New Products
and Solutions

Book Picks
Writing Stored Procedures for
Microsoft SQL Server
Matthew Shepker
SAMS

HK-Software Releases IBExpert


HK-Software announced the
availability of IBExpert, a Windows development tool for
InterBase databases.
IBExpert supports IB 4, 5, and
6 databases; works in different
databases at the same time; has
an insert into feature for temp
tables and for two databases; has
an editor for all database objects:
domains, tables, views,
stored procedures, triggers, generators, exceptions, and UDFs;
can export to Excel, RTF
(Word), HTML, CSV, SYLK,
DIF, TXT, and LaTeX; has a
Grant Manager and User Manager, Testdatagenerator, open
API for plug-ins with Delphi
source code sample, a stored
procedure debugger, performance analysis with graphical
output, backup and restore assistant, validation, a statistic assistant; is multilingual (German
and English, user-defined); has
BLOb viewer with support for

text, RTF, images, Hex; has


Code Insight, Code Completion, keyboard templates; SQL
history; SQL monitor; one-year
licensed user access on all
updates inclusive; and more.
A fully functional 30-day
trial version of IBExpert is avail-

able for download at http://


www.ibexpert.com.
HK-Software
Price: License for one computer, US$149;
contact HK-Software for multi-license pricing.
Contact: info@h-k.de
Web Site: http://www.ibexpert.com

Raize Software Releases CodeSite 2

ISBN: 0-672-31886-5
Cover Price: US$39.99
(346 pages)

B2B Application Integration


David Linthicum
Addison-Wesley

ISBN: 0-201-70936-8
Cover Price: US$39.95
(408 pages)

Raize Software, Inc.


announced the availability of
CodeSite 2, a debugging tool for
Delphi and C++Builder.
CodeSite 2 aids developers
locate problems in their code.
CodeSite 2 gives developers the
ability to send detailed information from within their application code to a specialized
receiver. In CodeSite 2, sending
information from your application code is still accomplished
using a TCodeSite object.
The TCodeSite class, which

defines all the methods available


for sending information, has
been significantly enhanced in
CodeSite 2. Developers can send
standard data types (e.g. integers, strings, etc.), objects, string
lists, bitmaps, registry entries,
memory blocks, text files, and
much more to the receiver without having to perform any data
conversions.
CodeSite 2 incorporates a
new architecture for transporting
messages from your application
code to the CodeSite Viewer.

Instead of sending messages


directly to the CodeSite Viewer,
TCodeSite objects send messages
to the new re-distributable
CodeSite Dispatcher, which is
responsible for routing all
CodeSite messages on a given
computer.
The transport architecture
in CodeSite 2 removes the
receiver aspect from the
CodeSite 1 Viewer and moves
it to the CodeSite Dispatcher.
In CodeSite 2, the CodeSite
Viewer becomes another destination. This change has several advantages. For example,
the Dispatcher has a significantly faster response time for
handling incoming messages
because there is no user interface to update. Another advantage to the new architecture
is that CodeSite now supports
sending messages to multiple
destinations. Messages can be
sent directly to a CodeSite
Log File.
CodeSite 2 architecture
changes are not limited to
Raize Software Releases CodeSite 2
continued on page 4

3 May 2001 Delphi Informant Magazine

Delphi
T O O L S
New Products
and Solutions

Book Picks
Special Edition Using XHTML
Molly E. Holzschlag
QUE

AbriaSoft Releases Abria MySQL Admin Training CD-ROM


AbriaSoft announced the
release of its Abria MySQL
Admin Training CD-ROM, the
first in a series of MySQL
education products. AbriaSoft is
the first company to commercially distribute MySQL in a
boxed package for online
database solutions.
Abria MySQL Admin CD-ROM
is an interactive computer-based
training CD-ROM designed for
users with some database experience who want to learn how
to administer a MySQL database
server. Abria MySQL Admin CD
guides users through the fundamental concepts of MySQL operations. Abria MySQL Admins 10
chapters cover topics that range
from installation, tools, and utilities to database security, restoration, and networking. In addition,
it provides troubleshooting advice
for any problems encountered
while working with MySQL.
Abria MySQL Admin Training

CD-ROM is available for purchase from the AbriaSoft corporate Web site. It is also included
in the Abria SQL Suite.
AbriaSoft also offers MySQL
instructor-led courses for corporations or groups. Future
MySQL education products will
include an additional computer-

based training CD-ROM called


Advanced MySQL Administration and Web-based training.
AbriaSoft
Price: US$59; downloadable version,
US$39.
Contact: sales@abriasoft.com
Web Site: http://www.abriasoft.com

ModelMaker Code Explorer for Delphi 5 Released


ISBN: 0-7897-2431-6
Cover Price: US$39.99
(958 pages)

XSLT: Working with XML and


HTML
Khun Yee Fung
Addison-Wesley

ISBN: 0-201-71103-6
Cover Price: US$39.95
(441 pages, CD-ROM)

4 May 2001 Delphi Informant Magazine

ModelMaker announced
the release of ModelMaker
Code Explorer Delphi 5. Code
Explorer is a substitute for

the standard Class Explorer


that ships with Delphi 5. It
shows classes (inheritance) and
members (fields, methods, and

Raize Software Releases CodeSite 2 (cont. from page 3)


just the transport method. The
CodeSite Viewer is completely
rewritten to overcome limitations of the earlier version.
There is no more limit to the
number of messages that can be
viewed in the CodeSite Viewer,
even on Windows 9x machines.
The new architecture of the
viewer also provides support for
advanced features such as multiple views, filters, and custom
inspectors.
CodeSite 2 comes in Standard
and Professional editions.
CodeSite 2 Professional adds
advanced features to the
CodeSite Viewer, including filtering, multiple views, and
custom inspectors (through the
use of an extensible plug-in
architecture). CodeSite 2 Professional also supports sending
CodeSite messages to remote

computers using TCP or UDP,


and sending messages via
HTTP to a special CodeSite
Web module running on a Web
server. The Web module redirects messages to their final
destination. CodeSite 2 Professional also includes the
source code for the CodeSite
Interface unit, which contains
the CodeSite Object (i.e. the
code that is linked into an
application to enable it to send
CodeSite messages).
CodeSite 2 supports Delphi 3,
4, and 5, and C++Builder 3, 4,
and 5.
Raize Software, Inc.
Price: CodeSite 2 Standard, US$149;
CodeSite 2 Professional, US$299. Check for
upgrading specials.
Contact: sales@raize.com
Web Site: http://www.raize.com/CodeSite/

properties) in two views and


allows Instant Editing.
Editing is simplified; the user
need only select the options in
dialog boxes and drag and drop.
The main features of Code
Explorer are the Create, Edit,
and Delete classes and members
with dialogs that validate input
and make entering data easy;
Editing properties that take
care of creating, deleting, and
updating fields and read/write
access methods; the copy/paste
or drag/drop members that
move from class to class with
conversions made where
needed; the ability to rearrange
and sort a class interface
and methods, either manually
or using predefined sorting
schemes; and highly improved
navigation and productivity.
A free fully functional demonstration is available at the
ModelMaker Web site.
ModelMaker
Price: US$75 per single-user license.
Contact: info@delphicase.com
Web Site: http://www.delphicase.com

On the Net
Web Applications / ASP / ISAPI / CGI / PHP / WebBroker / Delphi 5

By Dr Mark Brittingham

Writing Web Apps


Whats the Best Approach?

he pace of change in software development can make it difficult to keep your


bearings. In this article, Ill cover a variety of Internet and Web development
technologies, and show you that Delphi can fill an important niche in the new world
of Internet development.
For over 40 years, software development has been
largely a matter of creating programs for a specific
operating system and CPU in order to provide
services to an end user. The end user was typically a
human being who was directly interacting with the
CPU on which your software was running. Even
the client-server and middleware technologies that
arose during the early-to-middle 90s were linked in
a fairly static way back to the end user.
Thats why its no small exaggeration to say that
Web application technologies represent a paradigm
shift in software development. When developing
for the World Wide Web, your job as a Web developer is to program the users browser to behave
the way you want it to by sending it the appropriate HTML and JavaScript code. That is, youre not
developing the program directly; youre developing
a program that will, in turn, program the browser.
The ferment caused by this change has spawned
a variety of new technologies and new approaches
to software development. In this article, Ill discuss
these technologies as well as some ideas about the
future of Internet development.

Web Applications
The term Web applications is used to denote any
application written to program a remote browser.
By definition, this leaves out a number of technologies spawned by the Internet. For example, peer-topeer technologies like Napster and Gnutella owe
their creation to the ubiquity of the Internet, but
theyre still fairly traditional networking applications. Ill have more to say about Napster and
the social opportunities created by the Internet
in the final section of this article. For now, however, its important to focus on browser-based Web
5 May 2001 Delphi Informant Magazine

applications because theyre key to understanding


the advantages of the Internet: broad, effortless
distribution, and zero-configuration operation.
At the simplest level, a Web application requires
three things: a browser making requests, a server
producing HTML pages, and a network connecting the two that supports the HTTP protocol.
In contrast with normal applications, Web applications are stateless: once a page is downloaded to
the browser, the link back to the server is severed
and the page is on its own. This statelessness is a
big challenge in writing Web applications, and has
significantly influenced the technologies discussed
here.
In the history of Web application development,
the first temptation in some circles was to simply
circumvent the limitations of HTML by downloading and embedding network-aware applications into the browser. This was the genesis of
Java applets and ActiveX Internet technologies.
These embedded, client-side technologies released
the developer from many of the limitations of pure
HTML, but neither technology has made significant inroads into the Web application space. Security concerns and a lack of standardization have
conspired to restrict them to the fringe of Web
application development.

Server-side Technologies
In contrast to the disappointments of client-side
technologies, weve witnessed an explosion of
server-side technologies. All of these technologies
are based on the idea that a document sent to the
browser can be dynamically created by software,
rather than simply being read from a file.

On the Net
To reiterate, your job as a server-side developer is to deliver HTML
and JavaScript to the browser that makes it act the way you want
it to. Thats it; theres nothing mystical about it. You can get as
obscure and complicated as you want on the server side, and it
doesnt amount to a ham-hock at a vegetarian soire unless it delivers
HTML/JavaScript to the browser. This also means that you should
abandon any idea that you can succeed by just sticking with Delphi
or any other server-side technology. To be effective in this domain,
you must spend time mastering HTML and JavaScript, or your Web
applications will always be ugly or will perform badly. Of course,
since most Web applications are ugly and perform badly, you might
find that no one notices. But Ive got to believe you arent like that, or
Id lose all motivation to finish this article.

ND-IntraWeb
Just to demonstrate that I have a sense of humor, Ill kick off
this discussion of Web application technologies with the only one
that, indeed, does not require that you learn HTML and JavaScript.
ND-IntraWeb (from Nevrona Designs, http://www.nevrona.com/
intraweb) is a Delphi-specific solution that permits you to create and
use a Delphi application as a kind of Web server by hiding it
behind the ND-IntraWeb server. That is, people visiting your site will
see HTML pages that ND-IntraWeb creates based on your Delphi
forms. So, if they click on a button in the browser, ND-IntraWeb
will click the button on your application and deliver the resulting
form to the user.
The strength of ND-IntraWeb is that it permits you to leverage the
RAD nature of Delphi development in a Web application environment. By taking control of the production of your HTML and
JavaScript, ND-IntraWeb frees you to create applications with the
more directly visual Delphi RAD tools.
ND-IntraWeb has two weaknesses: the user interface presented to
your user, and the scalability of an ND-IntraWeb application. An
ND-IntraWeb application will look more like a Windows application
than the typically-more-creative and graphical layout of HTML Web
pages. While you can embed your application output in an HTML
page, you will still need to remain conscious of the differences in
the presentation of your work, if you dont want your application to
depart too jarringly from the HTML idiom.
ND-IntraWebs scalability is limited by the fact that it runs a new
copy of your application for each visitor to your site in order to
maintain a consistent state. Normal Web applications dont maintain
a great deal of information in memory for each user. Thus, it is
quite possible to serve many thousands of simultaneous users of your
system. In contrast, ND-IntraWeb is limited to 50-250 simultaneous
users, depending on the size of your application.
If your application is targeted at a small set of users (e.g. a sales
force automation application for a small- to medium-sized business)
and you have a rapidly-approaching deadline, you may well find NDIntraWeb to be an ideal solution.

Active Server Pages (ASP)


ASP applications are the most popular Microsoft-centric Web development technology in use today. In ASP applications, you embed
ASP code directly into your Web pages. Typically, this code is
VBScript, but other scripting languages are also available. When an
.asp page is requested, the Web server receiving the request retrieves
the document and submits it to the ASP DLL to interpret the script.
The results returned by the DLL are then sent to the browser.
6 May 2001 Delphi Informant Magazine

You use the ASP scripting language to add dynamic content to the
pages in your site. While ASP scripting is reasonably robust, this
technologys real power comes from its tight integration with COM.
The best ASP strategy is generally to implement the visual aspects of
your site with HTML, and to use COM components written in Delphi
to supply the dynamic content. Even Microsoft recommends that you
restrict your VBScript to simple tags and implement your program
logic in your COM objects. If youre committed to ASP, taking this
approach will also help you move up to ASP+ once its available.
Another big strength of ASP is its sheer popularity. There are many
helpful ASP sites on the Web and plenty of good books. Despite
Microsofts recommendation that you emphasize COM objects in
your ASP development, this technologys popularity is primarily due
to the fact that ASP permits you to write your code on the same
page as your HTML. Because both visual layout and code are all in
one place (except for any COM objects you have created), an entire
project can be built one page at a time.
The downsides to ASP are the speed of an interpreted script, and the fact
that you are likely to be working in Basic; the other scripting languages
are rarely used, and nearly all of the documentation is for VBScript.
And dont buy into any arguments over whether ASP is faster or
slower than the ISAPI technology (covered later in this article). The
ASP DLL is an ISAPI DLL, and so cannot, by definition, be faster.

Macromedia UltraDev
UltraDev isnt a development technology, but rather a tool for helping you build ASP pages (and others) by supporting the scripting
process. I cover it briefly because Ive met a number of people who
think that its indeed a new development technology, above and
beyond the technologies described here.
UltraDev (from Macromedia, http://www.macromedia.com/
software/ultradev) is more sophisticated than other Web development
environments because it permits you to work with live data sets. That
is, you dont just see a blank spot where the output of your dynamic
script would appear: the tool helps you make database-to-output links
so you actually see what the user would see (or close to it). As Internet
development tools go, this is a bit of a breakthrough and is why
UltraDev generated a lot of excitement when it was released.
Personally, Im only mildly excited about this. Since Web output
of database data is already fairly simple, there is less of a win here
than there was back when VB and Delphi brought form-based design
and RAD development to Windows. Nonetheless, its something to
consider especially if youre mostly a visual page designer rather
than an HTML hacker like me.

ColdFusion
ColdFusion (from Allaire Corp., http://www.coldfusion.com/Products/
coldfusion) is like ASP in that it is a scripting language implemented
by the ColdFusion Application Server, an application that interacts
with a Web server to interpret HTML files with embedded scripts. On
Windows NT, ColdFusion usually communicates with the Web server
via an ISAPI.DLL. ColdFusion is also available on various flavors of
UNIX. ColdFusion is best for small- to medium-sized projects because
the interpreter is relatively slow. You can scale ColdFusion, but you do
so only at great expense: multiple servers bound together with Cluster
Cats or a similar technology. Indeed, the ColdFusion people have gone
to such extraordinary lengths to convince the world that the technology
is scalable, that youre almost guaranteed it is its greatest weakness.

On the Net
ColdFusions greatest advantage is that its scripting language is quite
easy to learn. If youre coming from the HTML design world, and are
looking for an HTML-like, easy-to-learn language, ColdFusion would
be the tool to try. If this isnt your background, then Id suggest you give
it a pass. Its too expensive and too slow to be worth considering, even if
the language is somewhat easier to learn than ASP.

PHP
PHP is an open-source scripting language based on the C programming language syntax. PHPs main advantages are its power (especially in pattern matching) and its price (free). Its also available on a
large number of platforms.
The drawback to PHP is related to the benefit of its power: its underlying
scripting language is more complex than any of the other scripting
languages. Indeed, PHP scripting isnt any easier than the ISAPI development discussed below, and it doesnt have the advantages of ISAPIs
speed, or of Delphis advanced IDE and debugger. Nor does it have the
seamless integration with COM found in ASP (although you can spawn
external programs and retrieve the results). In addition, function calls
are relatively expensive in PHP, so any reasonably complex application
will either be implemented in one very long function, or youll incur a
significant performance penalty compared to the alternatives.

CGI Applications and Perl


CGI was the first dynamic content technology to arrive on the scene
as the Internet began its explosive growth. A CGI application is called
explicitly by a page request URI. For example, a CGI application call
might look like this:
http://www.somesite.com/CGI-BIN/CGIApp.exe

This call requests that the Web server spawn the CGIApp.exe
program and capture its output (on the console) for output to
the browser. WinCGI applications operate in essentially the same
manner, except the input/output are performed via the Windows
registry rather than via the console.
The advantage of CGI applications is that they are relatively simple
to create, support a broad array of development languages (Perl being
perhaps the most popular), and are available on any platform. The
biggest drawback to CGI applications is that every Web page request
results in the loading and initialization of a full executable. Thus,
CGI applications are, in general, the worst performing dynamic Web
technology available. Nevertheless, for small sites, CGI may well be
the fastest way to develop and deploy a dynamic application.

ISAPI
If you conclude that all of the technologies described so far suffer
from performance limitations, youre not far from the mark. While
ASP can be made considerably faster by using COM objects, theres
no technology quite so fast as working directly with the requests
coming from the users within the process boundary of the Web
server. This is exactly what ISAPI does. ISAPI is implemented as a
DLL thats used by the Web server, either when requested in the URL
(like CGI), or when a specific file extension is requested.
Things get interesting with ISAPI and a bit more difficult. As
previously mentioned, the Web is stateless; every page request is
a thing unto itself. Thats why ASP and ColdFusion (and others)
implement what is called state management to help you keep data
entered or requested by a user on one page in synch with information
presented or requested to that same user on other pages.
7 May 2001 Delphi Informant Magazine

The bad news is that ISAPI itself does not implement state management for you. The good news is that Delphis ISAPI framework,
WebBroker, implements state management in Delphi 6, and can
easily be extended to do so in Delphi 5 (see Real-world Web Apps
in the July 2000 Delphi Informant).
When you code ISAPI, you build your site in two parts: visual mode
(HTML) and code mode (coding the ISAPI DLL). You or your team
will create the HTML pages for your site with an HTML tool like
HomeSite (http://www.allaire.com/products/homesite/40) or DreamWeaver (http://www.macromedia.com/software/dreamweaver). These
HTML files will use tags wherever dynamic information will
appear. The Web browser doesnt request the HTML files, but instead
requests output directly from the ISAPI DLL. The DLL will retrieve
the files and replace any tags with the appropriate dynamic content.
HTML files handled in this way are often referred to as templates
a term discussed with Java and JSP later.
As you can see, the visual design of ISAPI Web applications is quite
separate from the logical design. Overall, the programming model
is extremely simple: get request and generate tag replacements. However, I strongly suggest you get a copy of the MDWeb libraries featured in my Real-world Web Apps article if youre using Delphi 5.
The library can be downloaded free from http://www.xapxone.com.
ISAPIs primary benefit is that it has the fastest page production
time of any Web application technology. If youre implementing a
medium-sized Web application (approximately 100,000 to 1,000,000
hits per day), you may well be able to provide this in a single ISAPI
DLL running on a single server, depending on the complexity of
your application.
As with all of the technologies discussed so far, you can scale an
ISAPI application easily enough with round-robin, or other router- or
switch-based request distribution methods.
By the way, if you choose ISAPI, download the Omni Web server
for development purposes. It integrates easily and nicely with Delphi
to make it trivially easy to test and debug your DLLs. If you dont do
this, but try to use PWS or IIS for debugging, youre likely to think
youve died and gone to Coding Hell. D6 promises a debugging environment that should eliminate the need for any external Web server,
but until then Omni is a great way to go: http://www.omnicron.ca.
If ISAPI is the fastest of all dynamic page delivery methods, and
its fairly easy to implement, you might be wondering why ISAPI
applications arent more common. One reason is surely that ISAPI
applications arent easy to create unless you have Delphi
and the WebBroker libraries. WebBroker, like the rest of the
VCL, makes a fairly complex technology far simpler than it
would otherwise be. Unfortunately, Delphi doesnt have a large mindshare in the Web development community, so ISAPI retains its
reputation as a difficult and time-consuming method for developing
Web applications.
The second reason ISAPI applications arent more common is that
even dynamic sites are themselves becoming dynamic. That is, personalization and content management technologies are being widely
deployed in large Web sites. These technologies work with dynamic
applications to manage real-time content customization and the
deployment of content by non-Web professionals. Its often not realistic to deploy an ISAPI-based solution in a large Web site. This would
preclude integration with the available personalization and content

On the Net
management packages which generally support only Java and ASP.
Of course, the vast majority of Web sites arent large enough (or rich
enough) to require such technology. For these Web sites, Delphi and
ISAPI are very well suited.
One last point: It isnt true, as some have said, that the smallest
error in an ISAPI DLL will crash the server. If youre working in
Delphi/WebBroker, your user will just get an error message.
If youre already familiar with Delphi, and youre willing to think in
terms of programming the browser, you should find Delphi ISAPI
development easy to master.

WebHub
Another technology often mentioned in connection with Delphi is
WebHub (from HREF Tools, http://www.webhub.com). WebHub
does one thing that I truly envy. It gives you essentially all of
the benefits of ISAPI, while letting you create EXEs instead. The
benefit of this is that you dont have to shut down your Web server,
or unload your DLL, every time you want to change your app.
Thus, your state information is preserved, and you can just debug
your executable.
WebHub has a fairly sophisticated request distribution mechanism, a
scripting language, and full session management. In some ways, you
can think of WebHub as trying to provide the enterprise environment
for Delphi that J2EE provides for Java (discussed next).
The biggest drawback to WebHub is that it adds complexity to
Delphi Web development without necessarily providing any advantage in scalability. That is, while there are a variety of software scalability tools in the product and the architecture is quite sophisticated,
my sense is that anyone looking for this kind of scalability would be
more likely to work with Java technologies. Java too has sophisticated
tools for distributing functionality among multiple servers, but it also
boasts a broad range of third-party tools for personalization, content
management, deployment, and performance monitoring.

Java, Servlets, and JSP


Java is a programming language, virtual machine, and component
model all rolled into one. Java and the Java server-side Web technologies are scalable, object-oriented, platform-neutral, and standardsbased.
Sun first introduced the Java servlet in 1996 as a way to provide
dynamic Web content within the Java programming model. A servlet
operates in essentially the same way as an ISAPI DLL in that it
accepts an HTTP request and produces suitable output in response.
The difference, of course, is that it does so in the context of a Java
Virtual Machine (actually, a servlet container).
While servlets are a powerful technology for coordinating the services
built into Java with the production of dynamic HTML code, its
expensive and unnecessary to maintain all HTML output in code.
Even relatively minor visual formatting changes require programming
changes (this is why the Delphi/WebBroker designers built in the
ability to use template files easily).
Taking a page from the ASP/COM playbook, Java Server Pages
(JSPs) let you embed tags and/or Java code in HTML template
pages. When a JSP template is requested, the Web server (or application server) will grab it, interpret the tags and/or Java code, compile it
into a servlet (usually caching it along the way), and then provide the
output in the same way that servlets do.
8 May 2001 Delphi Informant Magazine

The tags in Java Server Pages can be used to call Java objects (including Enterprise Java Beans) on the server to provide dynamic HTML
in much the same manner as ASP can call COM objects. The
advantage of this approach is that this makes it possible to build a
sophisticated environment of interacting Java objects natively built
and third-party and then plug the Web pages into this environment. Combined with J2EE-compliant application servers like those
from BEA, IBM, or Borland, this makes for a very powerful, scalable,
distributed technology.
While its certainly possible to build small- to medium-sized Web
applications in Java, I think its relevant for Delphi developers to ask
whether the transition to Java is worth the investment for such sites.
Delphi/WebBroker-based applications are faster and much easier to
deploy compared to JSP (at least on Windows-based servers), so there
are situations where a Delphi-based solution is clearly preferable.
Perhaps the most relevant question to ask when deciding between
Delphi/WebBroker and a JSP solution is whether your application
and/or site will require the use of third-party content management,
personalization, or other technologies that would be difficult to integrate with an ISAPI DLL.

The Future of Internet Application Development


Long ago in a previous life when I had a real job, as my wife
likes to say I worked on a distributed artificial intelligence system.
This system featured five computers interacting with one another
over a TCP/IP network, posting information on what we called a
bulletin board. As one system would post information, another
system would recognize something it could contribute, pull down the
posting, and replace it with the results of its own work. Actually, that
isnt quite the way it worked, but since it was a classified project I
cant actually tell you how it worked. But it makes for a useful sortof-true description. As you may have guessed, I had to learn a fair
amount about how TCP/IP worked, and how to implement what
was, essentially, a peer-to-peer system.
This is relevant not only because I might get you to buy me a
beer at a Borland conference in the vain hope that youll learn
what architecture we really used, but also because this development
made it clear that all Internet development hinges on simply defining
protocols for how two processes will communicate and a language
for what they will say. Email, FTP, Whois, HTTP, and the rest of
the alphabet soup of Internet protocols are simply ideas for how
two computers can communicate. The brilliance of Napster is not
its technology; its in recognizing that trading songs across a social
network is a good use for communicating computers. The same is
true for ICQ and Seti@home.
From my perspective, the future of Internet application development
lies with those who continue to see new possibilities, not only in internetworking, but in the very sociology of a distributed, computing society.

Microsofts .NET
Microsoft has recently pulled out all the stops in promoting its new
.NET framework. Were told that .NET represents the future of Internet and Web development. With .NET, Microsoft has indeed identified the key fulcrum point for future technological developments:
loosely coupled, network aware, component software. However, this
isnt Microsofts vision its Suns vision in creating its Java 2 Enterprise Edition (J2EE) specification. Of course, when it first struck me
that Microsoft might be building a derivative technology, I was, frankly,
stunned with disbelief. After recovering, however, I began to look for
indications of how this incipient battle would play out.

On the Net
At this point, I would suggest that .NETs success depends in part on
developments in handheld computing and Web appliances, application server technology, peer-to-peer technology, and Linux.
Handheld and appliance devices will be a vital part of future
distributed client computing and, having developed for the Palm,
Im certain that the PocketPC is poised to make significant inroads
in this area. Conversely, Suns handling of Java 2 Micro Edition
(J2ME) doesnt exactly inspire confidence. The Palm OS continues
to hold a majority market share, and theres little chance that
Microsoft will displace embedded Linux for the great majority
of Internet appliances. Thus, the future of small-footprint Internet-enabled clients will be browser-based technologies like WML,
rather than embedded Java or .NET applications. The reason is
simple: If no vendor wins a market majority in this space, the
only way to support them all is to support platform-independent
standards like HTTP, XML, and WML.
Web-based application servers will increasingly adopt the peer-topeer model within their processing boundaries. That is, under the
guise of Web services I predict a near explosion of Java beans and
.NET components for use in building large server systems. There
will be a strong incentive for organizations to standardize on one
or the other approach in order to maximize the interoperability
of their Web services components. If .NET is seriously delayed or
buggy, Java may well dominate the enterprise market to such an
extent that .NET is forever relegated to niche status. Currently, Java
does not enjoy this dominance and one should never underestimate
the capacity of UNIX/Linux vendors to shoot themselves in the
foot. At this point, the final outcome of the battle between .NET
and Java in the enterprise is highly uncertain, even though .NET is
merely a beta technology.
This implication of the shift to Web services in the enterprise is that
nearly all enterprise systems will become at least partially distributed.
Thus, rather than implementing dozens of disparate systems, enterprises will increasingly view their internal and external systems as a
Web of interacting subsystems. Within this Web, customer-oriented
applications will be almost exclusively Web applications (browserbased). Web applications will continue to grow in popularity for
internally deployed software. However, I also believe that well see a
resurgent interest in client and peer-to-peer applications built using
either Java or .NET. These desktops, often communicating with
servers and each other via XML, will define the next generation of
enterprise software.
Perhaps the smartest thing that Borland is doing for Delphi in this
developing arena is to support the use of EJBs in future editions of
Delphi. By linking Delphi into this model of computing, Borland
helps assure that Delphi will remain relevant in a Java-centric world.
Indeed, Delphi may well become the tool of choice for building fast,
compiled client applications that need to interact with Web services
delivered by enterprise application servers.

9 May 2001 Delphi Informant Magazine

If a future Delphi supports both EJBs and .NET, it could become an


essential tool for enterprises that need to work in both technologies.
While C# and VB may dominate in organizations using only .NET,
I dont believe that C#, in particular, spells the end for Delphi. In
any organization with a mix of .NET and Java systems, Delphi and
C++Builder will be the best way to tie all of these systems together.
On the consumer desktop, I dont believe that .NET will have an
impact for the next five to seven years. While consumer applications
will increasingly leverage Internet connectivity, relying on .NET will
be feasible only when the majority of desktops have it installed.
You need only observe that Windows 95 is still quite common on
consumer machines 6 years after its release to understand the barriers
to universal .NET deployment.
For small- to medium-sized businesses, Id like to see Delphi become
a popular technology for implementing dynamic Web applications.
However, the barriers to this remain very high. While I think Delphi/
ISAPI provides an extraordinary value proposition, it simply doesnt
have the mind share needed to compete with ASP, ASP+, ColdFusion, and other technologies. However, all technology is in such a
state of flux that one cannot rule out a happy outcome for Delphi
Web application development!
I do see Delphi gaining adherents as the limitations of Web application
development become apparent to small- to medium-sized businesses.
These businesses, always sensitive to value, will push some client application development back toward the client-server model, simply because
its more RAD and because its less expensive to deploy a data server
than a Web site. Only this time, the Internet will be the network and its
sheer ubiquity will drive us toward new explorations of networking and
deployment. Gaming platforms have already been exploiting this niche
for several years, but most other areas of computing still lag behind. Im
hopeful this will drive one of my favorite technologies, ASTA, into the
limelight. ASTA enables Internet-based, database-aware, multi-platform
computing. I believe its in exactly the right place at the right time.

In Closing
At this juncture, I cannot help but be excited by the future of
Internet application development. We are entering an era where
ubiquitous, standards-based computing will permit us to leap beyond
the construction of technological foundations, into the realm of
creative social computing. Even if youre making copies of your
resume on the back of now-worthless stock options, rest assured the
best and most creative ideas in computing still lie ahead.

Dr Mark Brittingham believes the secret of happiness is to radically change


careers every four to five years. He has worked at Bell Laboratories (now Lucent
Technologies) in Artificial Intelligence Research, at AT&T in user interface design,
and as president of Brittingham Software Design. He now does Web application
consulting in Delphi and Java, and rumor has it that his Web service, http://
www.DirectWellness.com, is the only profitable dot-com left in America.

The API Calls

Windows Services / Windows NT, 2000 / Delphi 4, 5

By Bill Todd

Managing Windows Services


A Component Wraps the API Functions for You

he Service and Service Application wizards in Delphi make writing Windows NT/2000
services easy. However, Delphi doesnt provide any tools to help you install and
control services. This article examines the Windows API functions that let you install,
remove, control, and configure services. It also presents a simple service manager
component (its class is named TdgServiceManager) you can use in your applications to
manage Windows services.
Once you have written a Windows service, you
need to install it before you can test it. Delphi
makes this easy if you want to install or remove
a service manually. All you have to do is run the
application from the command prompt with the
/Install command line switch. You can remove the
service by using the /Uninstall switch.

Adding a Service

To install a service programmatically, you need to


use the Windows CreateService function, which is
implemented in the AddTheService method, shown
in Listing One (on page 15).
The Windows Service Control Manager maintains information about all of the services on
the system in the Windows registry. Before
you can do anything with a service, you must
call OpenSCManager to get a handle to the Service Control Manager. The first parameter of
OpenSCManager is the name of the machine
whose services you want to work with. In Listing
One, the machine name comes from the private
member variable, FMachineName, that stores the
value of the components MachineName property. Since Windows API functions require nullterminated strings, FMachineName is cast to a
PChar in the call. Passing either nil or an empty
string opens the Service Control Manager on the
local machine.
10 May 2001 Delphi Informant Magazine

The second parameter is the name of the service


database you want to open. Passing nil opens the
database of active services. The last parameter is
the type of access you require, in this case the
constant SC_MANAGER_ALL_ACCESS is passed.
The access constants are declared in the WinSvc
unit, which you must add to your uses clause.
For a complete list of the access constants, see the
OpenSCManager function in the Win SDK help file
that comes with Delphi.
Some Windows services wont run unless one or
more other services are running. This implies that
services must be loaded in a particular order,
and that Windows shouldnt attempt to start a
service unless the services it depends on are running. Services can optionally be assigned to a
load order group. Load order groups are listed
in the registry at HKEY_LOCAL_MACHINE\
SYSTEM\CurrentControlSet\Control\
ServiceGroupOrder in the order in which they are
loaded. Load order groups are mainly for device
drivers, so you shouldnt need to use them.
One of the parameters passed to the CreateService
Windows API function is a list of services or load
order groups on which the service you are installing
depends. The list is in the form of a single string,
with the service names separated by a single null,
with two nulls after the last name. Load order

The API Calls


procedure TdgServiceManager.BuildDependencyList(
Dependencies: TStringList; var DependencyStr: PChar);
var
TotalLength, I, P : Integer;
Dest:
PChar;
begin
if Dependencies.Count = 0 then
begin
DependencyStr := nil;
Exit;
end
else begin
{ Compute bytes needed to hold all strings with a null
between each string and two nulls at the end. }
TotalLength := 0;
for I := 0 to Dependencies.Count - 1 do
TotalLength := TotalLength+Length(Dependencies[I]);
TotalLength := TotalLength + Dependencies.Count + 1;
{ Allocate the buffer and fill it with nulls. }
DependencyStr := AllocMem(TotalLength);
{ Copy strings from the string list to the PChar. }
P := 0;
for I := 0 to Dependencies.Count - 1 do begin
{ Set the PChar Dest to point to the location in
DependencyStr where the first character of the
next string should be placed. }
Dest := @DependencyStr[P];
{ Copy the string into DependencyStr starting at
the location pointed to by Dest (which is the
same as DependencyStr[P]). }
StrCopy(Dest, PChar(Dependencies[I]));
{ Increment P by the length of the string just
copied to DependencyStr plus one to allow for the
null at the end of the string. }
P := P + Length(Dependencies[I]) + 1;
end; // for
end; // if
end;

Figure 1: BuildDependencyList method.

group names must be prefixed with the SC_GROUP_IDENTIFIER


character. The Dependencies parameter in Listing One is a StringList,
and the call to BuildDependencyList converts the StringList to a nullseparated list as shown in Figure 1.
BuildDependencyList takes two parameters, the StringList that contains
the names of the services, and a PChar that will point to the nullseparated list. If the StringList is empty, the code sets the PChar to
nil and returns. The for loop iterates through the StringList and totals
the length of all of the strings. The next statement adds the Count
property of the StringList + 1 to the total length to account for the
null after each service name and the extra null at the end. Calling
AllocMem allocates a block of memory of the correct size, and assigns
its address to the PChar DependencyStr.
The for loop copies each string from the StringList to DependencyStr
as follows. The PChar Dest is set to the address of DependencyStr[P]
with P initialized to zero. Therefore, the first time through the loop
Dest points to the first character of DependencyStr. StrCopy copies
the string from the StringList to Dest. Next, P is incremented by the
length of the string that was just copied plus one, to skip the null
at the end of the string. Note that you dont have to assign null to
the character after the last character in the string, because AllocMem
initializes each byte in the block of memory it allocates to null.
The next time through the loop, Dest is again set to the address of
DependencyStr[P] so that Dest now points to the character following
the null at the end of the last string. This is the location for the first
character of the next string to be copied.
11 May 2001 Delphi Informant Magazine

procedure TdgServiceManager.ReleaseServiceHandle;
begin
if FServiceHandle <> 0 then begin
if not CloseServiceHandle(FServiceHandle) then
RaiseLastWin32Error;
FServiceHandle := 0;
if not CloseServiceHandle(FSCMHandle) then
RaiseLastWin32Error;
FSCMHandle := 0;
end;
end;

Figure 2: ReleaseServiceHandle method.

Returning to Listing One, the next step is to convert the


AccountName and Password parameters to null-terminated strings.
AccountName is the name of the user account you want the
service to run under, Password is the password for that account.
AccountName and Password require special handling because they
cannot be empty strings. However, if you want the service to
run under the local system account, you can pass nil for both
parameters. The code in Listing One copies the AccountName
and Password strings to the PChar variables, AccountNameStr and
PasswordStr, if they arent empty strings. If they are empty, the
PChar variables are set to null to indicate that this service should
use the system account.
Next, the Windows API function CreateService is called to add the
service to the Service Control Managers database. CreateService takes
13 parameters. The first is the Service Control Manager handle
obtained by calling OpenSCManager at the beginning of the method.
ServiceName is the name Windows uses to identify the service.
DisplayName is the name that appears in the Services Control Panel
applet on NT 4, or the Services application in Windows 2000.
Desired access is a DWORD constant that indicates the type of access
you want to the service. The constants are listed in the Win
SDK help file under CreateService. You will probably want to pass the
SERVICE_ALL_ACCESS constant so you can do anything to the service after it has been added. ServiceType is another constant that identifies the type of service. Use SERVICE_WIN32_OWN_PROCESS
if your service application contains a single service, and
SERVICE_WIN32_SHARE_PROCESS if the application contains
multiple services. If your service will interact with the desktop, use
SERVICE_INTERACTIVE_PROCESS. The other service types are
for device drivers (which are also services under Windows NT/2000).
StartType should be set to SERVICE_AUTO_START,
SERVICE_DEMAND_START (manual start), or
SERVICE_DISABLED. You will probably want ErrorControl
set to SERVICE_ERROR_NORMAL, which will display an
error message and log the error if the service cannot be started.
See the Win SDK help file for other options. The next parameter is the full path to the service EXE file.
The next two parameters are the load order group and tag ID. The
tag ID is used to indicate the order the service will load within
its load order group. Since load order groups are used mainly for
device drivers, nil is passed for both of these parameters, indicating that this service should be loaded after all of the services that
are in load order groups. The final three parameters are the list of
dependencies, the account name, and the password, all of which
have been described.

The API Calls


procedure TdgServiceManager.DeleteTheService;
begin
{ Get a handle to the service. If the handle cannot
be obtained, then exit. }
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else begin
if not DeleteService(FServiceHandle) then
RaiseLastWin32Error;
{ Close the handle to the service because the Service
Control Manager will not delete the service until
all handles are closed. }
ReleaseServiceHandle;
end;
end;

Figure 3: DeleteTheService method.

The CreateService call returns a handle to the service, which is stored


in the components private member variable FServiceHandle. Since all
of the Windows API calls that manipulate services require the Service
Control Manager handle or the handle to the service, both of these
values are stored in private member variables so they are accessible to
all of the methods of the service manager component. The handles are
closed when the MachineName or ServiceName properties are changed,
or when the service manager component is destroyed. You can also
close the handles by calling the ReleaseServiceHandle method shown in
Figure 2. This code calls CloseServiceHandle for each handle and sets
the handle variable to zero.

Deleting a Service
Deleting a service is much easier than adding one. The DeleteTheService
method shown in Figure 3 first gets a handle to the service by calling
the GetServiceHandle method, shown in Figure 4. It passes the handle
as the parameter to DeleteService, which flags the service for deletion.
The Service Control Manager will delete the service when it isnt
running, and when all handles to the service have been closed.
Since the service wont be deleted until all of the handles have been
closed, the method finishes by calling ReleaseServiceHandles. Note that
calling DeleteTheService will not delete the service immediately if it is running. If you want to delete the service immediately, call StopTheService
(described later in this article), then call DeleteTheService. You can do
this the other way around by calling DeleteTheService first and calling
StopTheService later. If you do this, however, call ReleaseServiceHandle
after calling StopTheService, because StopTheService has to open a handle
to the service in order to stop it.
With the exception of AddTheService, all of the methods that manipulate the service begin by calling GetServiceHandle. GetServiceHandle
exits if FServiceHandle is not 0, since a non-zero value means that the
handle is already open. This test works because the ReleaseServiceHandle
method sets FSCMHandle and FServiceHandle to zero after closing the
handles. If FServiceHandle is 0, a call to OpenSCManager gets a handle
to the ServiceControlManager. This handle is used as the first parameter
in the call to OpenService, which returns the handle to the service. The
second parameter passed to OpenService is the ServiceName property,
and the third is the constant that indicates what type of access you
want to the service.

Other Management Issues


Once your service has been installed, you can start, stop, pause, and
continue it using the StartTheService, StopTheService, PauseTheService,
12 May 2001 Delphi Informant Magazine

procedure TdgServiceManager.GetServiceHandle(
ServiceName: string);
begin
if FServiceHandle <> 0 then
Exit;
FSCMHandle := OpenSCManager(PChar(FMachineName), nil,
SC_MANAGER_ALL_ACCESS);
if FSCMHandle = 0 then
RaiseLastWin32Error
else begin
FServiceHandle := OpenService(FSCMHandle,
PChar(ServiceName), SERVICE_ALL_ACCESS);
if FServiceHandle = 0 then
RaiseLastWin32Error
end; // if
end;

Figure 4: GetServiceHandle method.


procedure TdgServiceManager.StartTheService;
var
PArgs: PChar;
begin
PArgs := nil;
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit;
if not StartService(FServiceHandle, 0, PArgs) then
RaiseLastWin32Error;
end;

Figure 5: StartTheService method.


procedure TdgServiceManager.StopTheService;
var
ServiceStatus: _SERVICE_STATUS;
begin
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else
if GetServiceStatus <> SERVICE_STOPPED then
if not ControlService(FServiceHandle,
SERVICE_CONTROL_STOP, ServiceStatus) then
RaiseLastWin32Error
else
begin
{ Wait for the service to stop. }
if not WaitForServiceStatus(SERVICE_STOPPED) then
raise EServiceStatusWaitError.Create(
'Service did not stop.');
end;
end;

Figure 6: StopTheService method.

and ContinueTheService methods. The StartTheService method (see


Figure 5) calls the Windows API StartService function and passes the
service handle as the first parameter. The second and third parameters
are used to pass startup arguments to the service. Its a feature thats
never used, so 0 is passed for the second parameter, and a null PChar
for the third parameter to indicate that there are no arguments.
Figure 6 shows the StopTheService method. StopTheService begins
by calling GetServiceStatus (see Figure 7) to ensure the service isnt
already stopped. If the service isnt stopped, the code calls the Windows API function ControlService, passing the service handle as
the first parameter, the constant SERVICE_CONTROL_STOP as
the second parameter, and a variable of type _SERVICE_STATUS
as the third parameter. The ServiceStatus variable receives the

The API Calls


function TdgServiceManager.GetServiceStatus: DWORD;
var
ServiceStatus: _SERVICE_STATUS;
begin
Result := 0;
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then Exit;
if not QueryServiceStatus(FServiceHandle,
ServiceStatus) then
RaiseLastWin32Error;
Result := ServiceStatus.dwCurrentState;
end;

Figure 7: GetServiceStatus method.

function TdgServiceManager.WaitForServiceStatus(
ServiceStatus: DWORD): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to FWaitTime do
if GetServiceStatus = ServiceStatus then
begin
Result := True;
Break;
end
else
Sleep(100);
end;

Figure 8: WaitForServiceStatus method.

status record showing the current status of the service when the
call returns. Finally, StopTheService calls the WaitForServiceStatus
method (see Figure 8), passing the SERVICE_STOPPED constant
to prevent the method from exiting until the service has actually
stopped.
Before going any farther, lets look at the GetServiceStatus and
WaitForServiceStatus methods in detail. GetServiceStatus, shown in
Figure 7, calls the Windows API QueryServiceStatus function, passing
the service handle as the first parameter and a variable, ServiceStatus,
of type _SERVICE_STATUS as the second. After the call, the
ServiceStatus.dwCurrentState field contains the current status of the
service and is assigned to Result. The constants that correspond to
the various states that the service can be in are listed in the Win
SDK help file under SERVICE_STATUS, as are the other fields in
the ServiceStatus record.
If youve ever started or stopped a Windows service manually, you
know it doesnt happen instantly; typically it takes a couple of seconds. To prevent users of the service manager component from
doing something that depends on a change in the state of a service
before the service actually changes, state all of the methods that
change the state of the service, such as StartTheService, then call
WaitForServiceStatus to prevent the method from returning until the
service has actually changed state.
WaitForServiceStatus, shown in Figure 8, takes the status constant to
wait for as a parameter and calls GetServiceStatus every 100 milliseconds
in a loop until the desired status is returned. The WaitTime property
of the service manager component controls the maximum time the
WaitForServiceStatus waits to ensure that it cannot loop indefinitely. If
WaitForServiceStatus times out, it returns False; otherwise it returns True.
13 May 2001 Delphi Informant Magazine

procedure TdgServiceManager.GetServiceConfiguration(
var StartType: DWORD; var Path, ServiceStartName,
DisplayName: string);
begin
{ Get the service configuration values. }
GetServiceConfig;
try
StartType := PCfg^.dwStartType;
{ Convert the strings from PChar to ANSI strings. }
Path := StrPas(PCfg^.lpBinaryPathName);
ServiceStartName := StrPas(PCfg^.lpServiceStartName);
DisplayName := StrPas(PCfg^.lpDisplayName);
finally
{ Free memory allocated by call to GetServiceConfig. }
FreeMem(PCfg);
end; // try
end;

Figure 9: GetServiceConfiguration calls GetServiceConfig.

procedure TdgServiceManager.GetServiceConfig;
{ Allocates a buffer and loads the service configuration
information into it. Note that any method that calls
this method is responsible for calling FreeMem(PCfg) to
free the memory allocated by the call to AllocMem. }
var
NumBytes: DWORD;
begin
{ Get a handle to the service. If no handle is
returned, then exit. }
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else begin
{ Call QueryServiceConfig to find out how much memory
is required to hold the configuration information.
The number of bytes required is returned in the last
parameter, NumBytes. }
QueryServiceConfig(FServiceHandle, nil, 0, NumBytes);
{ Allocate the required memory. }
PCfg := AllocMem(NumBytes);
{ Call QueryServiceConfig again passing memory buffer
and its size to get configuration information. }
if not QueryServiceConfig(FServiceHandle, PCfg,
NumBytes, NumBytes) then
RaiseLastWin32Error;
end;
end;

Figure 10: GetServiceConfig calls QueryServiceConfig.

You can also reconfigure a service using the GetServiceConfiguration


and SetServiceConfiguration methods. GetServiceConfiguration, shown
in Figure 9, returns the start type, path, account name, and display
name in its four var parameters. It calls GetServiceConfig, shown in
Figure 10, to get the configuration values, then assigns them to
the var parameters. The StrPas function is used to convert the
null-terminated strings to ANSI strings. In addition to these four
values, the QUERY_SERVICE_CONFIG record also contains the
service type, the error control type, the load order group, the tag
ID, and the dependencies. If you need any of these values, just
add additional parameters to GetServiceConfiguration, and assign
the values from the record to them.
GetServiceConfig, shown in Figure 10, calls the Windows API function, QueryServiceConfig, which fills a buffer with the configuration
information. QueryServiceConfig is unusual in that you must call it
twice, passing four parameters each time. The first parameter is the

The API Calls


numeric parameters that shouldnt be changed, pass the constant
SERVICE_NO_CHANGE. ChangeServiceConfig takes the same
values as parameters that QueryServiceConfig returns, with the addition of the password for the account the service will run under.
If you want to be able to change any of the configuration values
not passed as parameters to SetServiceConfiguration, just add the
parameters and pass them to ChangeServiceConfig. The parameters
are described in detail in the Win SDK help file.

Figure 11: The test applications main form.

service handle, the next is a pointer to a block of memory that will


hold the service configuration structure, the third is the size of the
block of memory, and the fourth is the required size of the block
of memory. The first call to QueryServiceConfig passes nil for the
memory pointer, and 0 for the size. The sole purpose of this call
is to get the value in the fourth parameter, NumBytes, so you will
know how much memory you need.
PCfg is a private member variable of type PQueryServiceConfig,
which is a pointer to a QUERY_SERVICE_CONFIG record.
Because PCfg is a pointer, you wont need to use the ^ operator
(as used in Figure 9) to indicate the value to which PCfg points.
The call to AllocMem allocates a block of memory NumBytes in
size and assigns the address to PCfg. Now that a block of memory
large enough to hold the configuration information is available,
the second call to QueryServiceConfig passes PCfg as the pointer to
the block of memory and NumBytes as its size, and retrieves the
configuration information.
The SetServiceConfig method shown in Listing Two (on page 15)
lets you change all of the configuration values. SetServiceConfig
uses the Windows API function ChangeServiceConfig to change the
configuration values. All of the string parameters you dont want to
change must be set to nil. To accomplish this, a PChar variable
is declared for each string parameter. The series of if statements
checks each parameter to see if its an empty string. If the string
parameter is empty, the PChar is set to nil. If the parameter
isnt empty, the PChar is set to the parameters value. For

14 May 2001 Delphi Informant Magazine

Listing Three (beginning on page 15) shows the complete code


for the TdgServiceManager component. Figure 11 shows the main
form of the sample application. It provides a simple way to test
the features of the service manager component. Note that if you
want to use the Install button, you must change the path in the
constant ServicePath at the beginning of the implementation section of the main forms unit to point to the EXE of the service you
want to install. A simple Windows service that you can test with
is also included. The service does nothing but issue a beep every
two seconds, but it provides a harmless service to install, remove,
start, stop, and configure.

Conclusion
If you need to manipulate Windows services in code, whether or not
theyre services you have written, the Windows API provides all of the
functions you need. The TdgServiceManager component described
in this article provides a simple wrapper around these functions to
make using them easier for Delphi programmers. For more detailed
information on the Windows API functions, look up the function in
the Windows SDK help file that comes with Delphi.
The files referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAY\
DI200105BT.

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 and over 80 articles, and is a member of Team Borland, providing
technical support on the 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.net.

The API Calls


Begin Listing One AddTheService method
procedure TdgServiceManager.AddTheService(
DisplayName: string; DesiredAccess, ServiceType: DWORD;
StartType: DWORD; ErrorControl: DWORD; Path: string;
Dependencies: TStringList; AccountName,
Password: string);
var
AccountNameStr: PChar;
PasswordStr: PChar;
DependencyStr: PChar;
begin
{ Get a handle to the Service Control Manager. }
FSCMHandle := OpenSCManager(PChar(FMachineName), nil,
SC_MANAGER_ALL_ACCESS);
if FSCMHandle = 0 then
RaiseLastWin32Error
else begin
{ Convert dependency list from StringList to PChar. }
BuildDependencyList(Dependencies, DependencyStr);
try
if AccountName = '' then
AccountNameStr := nil
else
AccountNameStr := PChar(AccountName);
if Password = '' then
PasswordStr := nil
else
PasswordStr := PChar(Password);
{ Create the service. }
FServiceHandle := CreateService(FSCMHandle,
PChar(FServiceName), PChar(DisplayName),
DesiredAccess, ServiceType, StartType,
ErrorControl, PChar(Path), nil, nil, DependencyStr,
AccountNameStr, PasswordStr);
if FServiceHandle = 0 then
RaiseLastWin32Error;
finally
FreeMem(DependencyStr);
end; // try
end; // if
end;

End Listing One


Begin Listing Two SetServiceConfig
procedure TdgServiceManager.SetServiceConfiguration(
StartType: DWORD; Path, ServiceStartName, Password,
DisplayName: string);
var
PathStr:
PChar;
StartNameStr:
PChar;
PasswordStr:
PChar;
DisplayNameStr: PChar;
SCLock:
SC_LOCK;
begin
{ Get a handle to the service. If the handle cannot
be obtained, then exit. }
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else begin
{ For each string parameter, see if the value is a null
string. If so, assume the user doesn't want to change
that value and set the parameter to nil. }
if Path = '' then
PathStr := nil
else
PathStr := PChar(Path);
if ServiceStartName = '' then
StartNameStr := nil
else
StartNameStr := PChar(ServiceStartName);
if Password = '' then
PasswordStr := nil

15 May 2001 Delphi Informant Magazine

else
PasswordStr := PChar(Password);
if DisplayName = '' then
DisplayNameStr := nil
else
DisplayNameStr := PChar(DisplayName);
SCLock := LockServiceDatabase(FSCMHandle);
if SCLock = nil then
RaiseLastWin32Error;
try
{ Change the configuration. }
if not ChangeServiceConfig(FServiceHandle,
SERVICE_NO_CHANGE, StartType, SERVICE_NO_CHANGE,
PathStr, nil, nil, nil, StartNameStr,
PasswordStr, DisplayNameStr) then
RaiseLastWin32Error;
finally
if not UnlockServiceDatabase(SCLock) then
RaiseLastWin32Error;
end; // try
end; // if
end;

End Listing Two


Begin Listing Three The TdgServiceManager
component
unit dgServiceManager;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, WinSvc;
type
EServiceManagerError = class(Exception);
EServiceStatusWaitError = class(EServiceManagerError);
EServicePauseError = class(EServiceManagerError);
TdgServiceManager = class(TComponent)
private
PCfg:
PQueryServiceConfig;
FMachineName:
string;
FSCMHandle:
SC_HANDLE;
FServiceHandle:
SC_HANDLE;
FServiceName:
string;
FWaitTime:
Integer;
FArguments:
TStrings;
FActive: Boolean;
function WaitForServiceStatus(ServiceStatus: DWORD):
Boolean;
function GetWaitTime: Integer;
procedure SetWaitTime(const Value: Integer);
procedure GetServiceConfig;
procedure BuildDependencyList(
Dependencies: TStringList; var DependencyStr: PChar);
procedure GetServiceHandle(ServiceName: string);
procedure SetMachineName(const Value: string);
procedure SetServiceName(const Value: string);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddTheService(DisplayName: string;
DesiredAccess, ServiceType: DWORD; StartType: DWORD;
ErrorControl: DWORD; Path: string;
Dependencies: TStringList; AccountName,
Password: string);
procedure ContinueTheService;
procedure DeleteTheService;
procedure GetServiceConfiguration(var StartType: DWORD;
var Path, ServiceStartName, DisplayName: string);
function GetServiceStatus: DWORD;

The API Calls


procedure PauseTheService;
procedure ReleaseServiceHandle;
procedure SetServiceConfiguration(StartType: DWORD;
Path, ServiceStartName, Password,
DisplayName: string);
procedure StartTheService;
procedure StopTheService;
published
{ Published declarations }
property MachineName: string
read FMachineName write SetMachineName;
property ServiceName: string
read FServiceName write SetServiceName;
property WaitTime: Integer
read GetWaitTime write SetWaitTime default 5000;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('DI', [TdgServiceManager]);
end;
{ TdgServiceManager }
constructor TdgServiceManager.Create(AOwner: TComponent);
begin
inherited;
WaitTime := 5000;
FArguments := TStringList.Create;
FServiceHandle := 0;
FSCMHandle := 0;
end;
destructor TdgServiceManager.Destroy;
begin
FArguments.Free;
if FServiceHandle <> 0 then ReleaseServiceHandle;
inherited;
end;
procedure TdgServiceManager.GetServiceHandle(
ServiceName: string);
begin
if FServiceHandle <> 0 then Exit;
FSCMHandle := OpenSCManager(PChar(FMachineName), nil,
SC_MANAGER_ALL_ACCESS);
if FSCMHandle = 0 then
RaiseLastWin32Error
else begin
FServiceHandle := OpenService(FSCMHandle,
PChar(ServiceName), SERVICE_ALL_ACCESS);
if FServiceHandle = 0 then
RaiseLastWin32Error
end; // if
end;
procedure TdgServiceManager.ReleaseServiceHandle;
begin
if FServiceHandle <> 0 then
begin
if not CloseServiceHandle(FServiceHandle) then
RaiseLastWin32Error;
FServiceHandle := 0;
if not CloseServiceHandle(FSCMHandle) then
RaiseLastWin32Error;
FSCMHandle := 0;
end; // if
end;
function TdgServiceManager.GetServiceStatus: DWORD;
var
ServiceStatus: _SERVICE_STATUS;
begin

16 May 2001 Delphi Informant Magazine

Result := 0;
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then Exit;
if not QueryServiceStatus(FServiceHandle, ServiceStatus) then
RaiseLastWin32Error;
Result := ServiceStatus.dwCurrentState;
end;
function TdgServiceManager.WaitForServiceStatus(
ServiceStatus: DWORD): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to FWaitTime do begin
if GetServiceStatus = ServiceStatus then
begin
Result := True;
Break;
end
else
Sleep(100);
end; // for
end;
procedure TdgServiceManager.StartTheService;
var
PArgs: PChar;
begin
PArgs := nil;
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then Exit;
if not StartService(FServiceHandle, 0, PArgs) then
RaiseLastWin32Error;
end;
procedure TdgServiceManager.StopTheService;
var
ServiceStatus: _SERVICE_STATUS;
begin
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else begin
if GetServiceStatus <> SERVICE_STOPPED then
begin
if not ControlService(FServiceHandle,
SERVICE_CONTROL_STOP, ServiceStatus) then
RaiseLastWin32Error
else begin
{ Wait for the service to stop. }
if not WaitForServiceStatus(SERVICE_STOPPED) then
raise EServiceStatusWaitError.Create(
'Service did not stop.');
end; // if
end; // if
end; // if
end;
procedure TdgServiceManager.PauseTheService;
var
ServiceStatus: _SERVICE_STATUS;
begin
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else
if GetServiceStatus = SERVICE_RUNNING then
if not ControlService(FServiceHandle,
SERVICE_CONTROL_PAUSE, ServiceStatus) then
RaiseLastWin32Error
else
{ Wait for the service to pause. }
if not WaitForServiceStatus(SERVICE_PAUSED) then
raise EServiceStatusWaitError.Create(
'Service did not pause.');

The API Calls


else
raise EServicePauseError.Create(
'The service is not running or cannot be
paused.');
end;
procedure TdgServiceManager.ContinueTheService;
var
ServiceStatus: _SERVICE_STATUS;
begin
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else
if GetServiceStatus = SERVICE_PAUSED then
if not ControlService(FServiceHandle,
SERVICE_CONTROL_CONTINUE, ServiceStatus) then
RaiseLastWin32Error
else
{ Wait for the service to continue. }
if not WaitForServiceStatus(SERVICE_RUNNING) then
raise EServiceStatusWaitError.Create(
'Service did not continue.');
else
raise EServicePauseError.Create(
'The service is not paused.');
end;
function TdgServiceManager.GetWaitTime: Integer;
begin
Result := FWaitTime * 100;
end;
procedure TdgServiceManager.SetWaitTime(const Value:
Integer);
begin
FWaitTime := Round(Value div 100);
if FWaitTime < 1 then FWaitTime := 1;
end;
procedure TdgServiceManager.GetServiceConfig;
{ Allocates a buffer and loads the service configuration
information into it. Note that any method that calls this
method is responsible for calling FreeMem(PCfg) to free
the memory allocated by the call to AllocMem. }
var
NumBytes: DWORD;
begin
{ Get a handle to the service. If no handle is returned,
then exit. }
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else begin
{ Call QueryServiceConfig to find out how much memory
is required to hold the configuration information.
The number of bytes required is returned in the last
parameter, NumBytes. }
QueryServiceConfig(FServiceHandle, nil, 0, NumBytes);
{ Allocate the required memory. }
PCfg := AllocMem(NumBytes);
{ Call QueryServiceConfig again passing memory buffer
and its size to get configuration information. }
if not QueryServiceConfig(FServiceHandle, PCfg,
NumBytes,
NumBytes) then
RaiseLastWin32Error;
end; // if
end;
procedure TdgServiceManager.GetServiceConfiguration(
var StartType: DWORD; var Path, ServiceStartName,
DisplayName: string);
begin
{ Get the service configuration values. }

17 May 2001 Delphi Informant Magazine

GetServiceConfig;
try
StartType := PCfg^.dwStartType;
{ Convert the strings from PChar to ANSI strings. }
Path := StrPas(PCfg^.lpBinaryPathName);
ServiceStartName := StrPas(PCfg^.lpServiceStartName);
DisplayName := StrPas(PCfg^.lpDisplayName);
finally
{ Free memory allocated by call to GetServiceConfig. }
FreeMem(PCfg);
end; // try
end;
procedure TdgServiceManager.SetServiceConfiguration(
StartType: DWORD; Path, ServiceStartName, Password,
DisplayName: string);
var
PathStr:
PChar;
StartNameStr:
PChar;
PasswordStr:
PChar;
DisplayNameStr: PChar;
SCLock:
SC_LOCK;
begin
{ Get a handle to the service. If the handle could not be
obtained then exit. }
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else begin
{ For each string parameter, see if the value is a null
string. If so, assume user doesn't want to change
that value and set the parameter to nil. }
if Path = '' then
PathStr := nil
else
PathStr := PChar(Path);
if ServiceStartName = '' then
StartNameStr := nil
else
StartNameStr := PChar(ServiceStartName);
if Password = '' then
PasswordStr := nil
else
PasswordStr := PChar(Password);
if DisplayName = '' then
DisplayNameStr := nil
else
DisplayNameStr := PChar(DisplayName);
SCLock := LockServiceDatabase(FSCMHandle);
if SCLock = nil then
RaiseLastWin32Error;
try
{ Change the configuration. }
if not ChangeServiceConfig(FServiceHandle,
SERVICE_NO_CHANGE, StartType, SERVICE_NO_CHANGE,
PathStr, nil, nil, nil, StartNameStr,
PasswordStr, DisplayNameStr) then
RaiseLastWin32Error;
finally
if not UnlockServiceDatabase(SCLock) then
RaiseLastWin32Error;
end; // try
end; // if
end;
procedure TdgServiceManager.DeleteTheService;
begin
{ Get a handle to the service. If the handle could not
be obtained then exit. }
GetServiceHandle(FServiceName);
if FServiceHandle = 0 then
Exit
else begin
if not DeleteService(FServiceHandle) then
RaiseLastWin32Error;

The API Calls


{ Close handle to the service because the Service
Control Manager won't delete the service until all
handles are closed. }
ReleaseServiceHandle;
end;
end;
procedure TdgServiceManager.BuildDependencyList(
Dependencies: TStringList; var DependencyStr: PChar);
var
TotalLength: Integer;
I:
Integer;
P:
Integer;
Dest:
PChar;
begin
if Dependencies.Count = 0 then
begin
DependencyStr := nil;
Exit;
end
else begin
{ Compute number of bytes needed to hold all strings
with a null between each string and two nulls at
the end. }
TotalLength := 0;
for I := 0 to Dependencies.Count - 1 do
TotalLength := TotalLength + Length(Dependencies[I]);
TotalLength := TotalLength + Dependencies.Count + 1;
{ Allocate the buffer and fill it with nulls. }
DependencyStr := AllocMem(TotalLength);
{ Copy the strings from the string list to the PChar. }
P := 0;
for I := 0 to Dependencies.Count - 1 do begin
{ Set the PChar Dest to point to location in
DependencyStr where first character of the next
string should be placed. }
Dest := @DependencyStr[P];
{ Copy the string into DependencyStr starting at the
location pointed to by Dest (which is the same as
DependencyStr[P]). }
StrCopy(Dest, PChar(Dependencies[I]));
{ Increment P by the length of the string just copied
to DependencyStr plus one to allow for the null at
the end of the string. }
P := P + Length(Dependencies[I]) + 1;
end; // for
end; // if
end;
procedure TdgServiceManager.AddTheService(
DisplayName: string; DesiredAccess, ServiceType: DWORD;
StartType: DWORD; ErrorControl: DWORD; Path: string;
Dependencies: TStringList;

18 May 2001 Delphi Informant Magazine

AccountName, Password: string);


var
AccountNameStr: PChar;
PasswordStr:
PChar;
DependencyStr: PChar;
begin
{ Get a handle to the Service Control Manager. }
FSCMHandle := OpenSCManager(PChar(FMachineName), nil,
SC_MANAGER_ALL_ACCESS);
if FSCMHandle = 0 then
RaiseLastWin32Error
else begin
{ Convert dependency list from StringList to PChar. }
BuildDependencyList(Dependencies, DependencyStr);
try
if AccountName = '' then
AccountNameStr := nil
else
AccountNameStr := PChar(AccountName);
if Password = '' then
PasswordStr := nil
else
PasswordStr := PChar(Password);
{ Create the service. }
FServiceHandle := CreateService(FSCMHandle,
PChar(FServiceName), PChar(DisplayName),
DesiredAccess, ServiceType, StartType,
ErrorControl, PChar(Path), nil, nil, DependencyStr,
AccountNameStr, PasswordStr);
if FServiceHandle = 0 then
RaiseLastWin32Error;
finally
FreeMem(DependencyStr);
end; // try
end; // if
end;
procedure TdgServiceManager.SetMachineName(
const Value: string);
begin
FMachineName := Value;
ReleaseServiceHandle;
end;
procedure TdgServiceManager.SetServiceName(
const Value: string);
begin
FServiceName := Value;
ReleaseServiceHandle;
end;
end.

End Listing Three

Distributed Delphi
MTS / COM+ / ADO / Microsoft Access / Windows NT 4 / Windows 2000 / Delphi 4, 5

By Malcolm Matthews

Creating MTS/COM+
Components
Error Handling, Debugging, Installation, and More

his article examines how to write MTS components for Windows NT 4, and the new
COM+ components for Windows 2000. It begins with an explanation of MTS and the
theory behind it. Then the creation of MTS components is described, including tips on how
to reduce the number of problems you can encounter.
Topics covered include error handling, debugging, security, and installation of the components. This article assumes a basic understanding
of COM and the creation of COM components
with Delphi. The concepts are demonstrated
with a simple database component that uses
ADO to access an Access database.

What Is MTS?
Microsoft Transaction Server is far more than just a
transaction server. Its a COM component hosting
environment that:
 manages system resources, e.g. processes,
threads, and connections;
 manages server object creation, execution, and
deletion;
 automatically initiates and controls transactions;

Workstation
MTS Server

Laptop computer

19 May 2001 Delphi Informant Magazine

implements security so unauthorized users


cannot access the application; and
provides tools for configuring, managing,
and deploying the applications components.

The key benefit of MTS is that it frees developers from many of these aspects of middle-tier
application development, allowing them to
concentrate on the business logic theyre
implementing.

What Does MTS Do?


MTS forms the middle tier of a distributed system,
and hosts the COM components that do the work.
In a typical system (shown in Figure 1), these will:
 take data from the database;
 pass it on to the (thin) client applications;
 receive data back from the clients;
 apply business rules and processing; and
 save it back to the database.
Consider for a moment what each of the client applications is doing. Most of the time theyre waiting for
the users to think or type. If each client application
has a dedicated component in the middle tier and
a corresponding database connection, those resources
are being wasted most of the time.

Computer

Figure 1: A typical MTS application.

Database Server

MTS allows us to drastically reduce the number


of components and database connections by
sharing them among the users. On small systems
this isnt significant, but for large systems
(Microsoft suggests more than 100 users) this
can make a big difference regarding the resources
required to support each user.

Distributed Delphi
Whats a Component?
Components arent objects resulting from object-oriented analyses. A typical object has methods and properties. Consider a
customer object thats installed in the middle tier. If we want
to find the customers name, address, and telephone number, we
need to call a method to instantiate it with the data for the
required customer, retrieve the name property, then the address
property, and finally the telephone number. Each of these requires
a network round trip. That adds up to four return trips, which
takes too much time and is inefficient.
In practice, a single method call that returns all of the data is
used. While this may result in unnecessary data being fetched, the
performance benefits far outweigh the possible waste.
Using a single method call has another benefit upon which MTS
capitalizes. In the usual object model, the object must store the
data, ready to supply more when required. This means it can only
interact easily with one client. Returning all of the data at one
time means it doesnt need to retain any information; its stateless
and therefore free to act for another client. This is at the heart of
MTS. Thus business objects become glorified code libraries and
are usually referred to as components.

Resource Management
MTS usually destroys a component when it has finished a method
call for a client. This sounds crazy, and it would be if the components had to be completely re-created each time, including
all their resources (such as threads and database connections)
especially database connections as theyre very expensive to
establish. To overcome this, MTS pools many of its resources. So
when a new component requires a database connection, its given
an existing one. When its finished with it, MTS takes it back
instead of closing it. The threads used to run the components are
also pooled in this way.
Consequently, the overhead of repeatedly creating and destroying
components is reduced, and when the number of client connections is high enough, is fully compensated by the reduced
resources. The load on the database will also be reduced.
Understanding the way MTS creates and frees components, and
pools the supporting resources, is key to creating effective MTS
components. Note: For database connection pooling to work, the
database driver must support it.

MTS
Client Application

Context Object

MTS Component

Figure 2: The MTS context object.

20 May 2001 Delphi Informant Magazine

Figure 3 (Top): The ActiveX page of the New Items dialog box.
Figure 4 (Bottom): Launching the MTS Object wizard.

COM+
MTS runs on Microsoft Windows NT 4. For Windows 2000, Microsoft has merged it into the COM infrastructure itself to create
COM+. This contains all of the features of MTS, plus a few more. So
what was previously an MTS package becomes a COM+ application.
As no changes are required for MTS components to run on Windows
2000, lets consider them the same thing.
One of the new features in COM+ is that components as well
as database connections can be pooled; this leads to significant
performance increases. MTS components are dynamically created
and destroyed each time theyre required for Just-in-Time Activation. Any resources they use are pooled and shared between the
components as they run. Before we look at creating MTS components using Delphi, there is one aspect of the details of the way
that this works to understand.
When a client application creates a COM object, it receives a reference to that object, or more correctly an interface to that
object. If the object is destroyed, this reference becomes invalid and
subsequent method calls will fail. MTS needs to do something to
avoid this. It also needs to maintain some state information for the
client application, such as security and transaction information.

Distributed Delphi
Method
DisableCommit
EnableCommit
IsCallerInRole
IsInTransaction

IsSecurityEnabled

SetAbort
SetComplete

Description
Used for stateful components;
beyond articles scope
Used for stateful components;
beyond articles scope
Used for security checking within
components
Used to check if MTS has created a
transaction; transaction model can be
changed after installation
Used to check if security checking has been
activated; it is also configurable after
installation
Tells MTS the component has finished and
wants the transaction aborted
Tells MTS the component has finished and
wants the transaction committed

Figure 7: TMtsAutoObject introduces seven methods that deal


with the context object.

The first step, as with any COM component, is to decide whether it


should be an in-process or out-of-process server. With MTS there is
no choice, it must be an in-process server. So the first thing to create
is an ActiveX Library from the ActiveX tab of the New Items dialog
box, as shown in Figure 3.
We now need to create an MTS component. There are two
options for this: an MTS Object, and an MTS Data Module.
Theyre both on the Multitier tab of the New Items dialog box
(see Figure 4). The MTS Data Module is intended primarily for
MTS MIDAS applications, so well use the MTS Object, as shown
in Figure 5.
Figure 5 (Top): Specifying the CoClass name and threading
model. Figure 6 (Bottom): Creating GetData in Delphis Type
Library Editor.

This is where the context object comes in. Although not strictly
accurate, I think of this as pretending to be the MTS component, so
the clients reference is always valid (as illustrated in Figure 2).
When the client first creates an MTS component, the context object
is created. It exists while the client holds a reference to the component. When a method is requested, the context object creates the
component, runs the method, and then destroys the component. The
exact operation of this object and MTS is more complicated, but this
simplification helps and is perfectly adequate.
It may appear that having a context object for each client violates the
idea of reducing resources by Just-in-Time Activation. However, the
context objects are very small, and therefore impose far less load on
the MTS server than the actual MTS components.

Creating MTS Components with Delphi


For all this to work, any MTS component we create must communicate with MTS via the context object, which is a COM object
and is accessed by an interface. Not surprisingly, Delphi ships with
classes and wizards to assist with all of this. The base classes that we
can inherit from in Delphi (TMtsAutoObject and TMtsDataModule)
have already done this work interfacing with the context object, leaving us to call methods that we inherit. These classes also implement
the IObjectControl interface that MTS uses to control components.
21 May 2001 Delphi Informant Magazine

As with any COM object, we need to specify the CoClass name


and the threading model. With MTS, you usually select Apartment
as the threading model. With COM+ you can use Apartment, but
it will not be possible to pool the component. For this we must
use Both, which creates a multi-threaded apartment COM object
that supports single-threaded apartments. (A description of the
differences between STA and MTA models is beyond the scope
of this article.)
The transaction model can also be specified at this point, although
it can easily be changed when the component is installed in MTS.
This setting governs whether MTS creates a transaction for the
component each time a method is called. Well look at this in
more detail later.
As a rule of thumb, if the component will be updating the database, Requires a transaction will be selected; otherwise Supports
transactions will provide better performance. The reason for this
is that MTS uses distributed transactions that allow updates to
more than one database in the transaction. This unfortunately
imposes a big overhead. Because this attribute is specified at the
component level, it can be a good idea to put update methods
into separate components, so transactions are only created when
required. Event support code is not usually required for MTS
components.
Having completed this wizard, the usual Type Library Editor will
appear allowing methods to be created. Remember, the component
is to be stateless, so properties arent usually used, although you can
create read-only ones and still have a stateless component.

Distributed Delphi
function TMTSDemo1.GetData: OleVariant;
begin
try
// Main processing goes here.
SetComplete;
except
SetAbort;
Raise;
end;
end;

Figure 8: Skeleton to use for MTS component methods exposed


through the COM interface.
function TMTSDemo1.GetData: OleVariant;
begin
try
Result := _GetData;
SetComplete;
except
SetAbort;
Raise;
end;
end;

Figure 9: Using an underscore to identify the internal routine name.

For this example, well create a single method named GetData that
returns a variant (see Figure 6). This will actually be an ADO
Recordset. I could include the ADO type library on the Uses tab of
the Type Library Editor, and specify the return type as Recordset.
However, a variant is simpler for this example.
(Note: Delphi 4 had problems returning exceptions to the client
application when the return type was Recordset. It blew up the
MTS component! I dont recommend using Delphi 4 for MTS
components anyway; the MTS COM components it produces are
prone to resource leaks.)

Writing Methods

Having created the method skeleton using the Type Library Editor,
we now need to write the code. The most important thing is
to tell MTS when weve finished, so it knows it can destroy the
component (or put it back into the pool). If there was a transaction,
we also need to tell MTS if we want the transaction to be committed or rolled back. If we dont, MTS will treat the component as
stateful, and we wont gain the resource savings. Figure 7 provides
more information on context object methods.
We need to use the last two methods, SetAbort and SetComplete,
here. Its a good idea to use the skeleton in Figure 8 for each method
in an MTS component thats exposed through the COM interface.
If anything goes wrong, MTS is told, and an exception is raised and
marshaled across the COM interface to the client.
SetAbort and SetComplete act as votes in the outcome of the transaction. More than one component can participate in the transaction, as we shall see shortly. As soon as one of them calls SetAbort,
the transaction is rolled back. This includes work by all components in the transaction. No more database activity is allowed after
this, so its important that SetAbort is only called when needed,
and that any exception handling doesnt allow further database
activity. If the component does try to access the database, a helpful
$8004E004 exception will be raised. This is defined in the MTX
unit, but other documentation is a little scarce.
22 May 2001 Delphi Informant Magazine

function TMTSDemo1._GetData: OleVariant;


var
Data : Recordset;
StrConnectionString : string;
begin
{ An ODBC DSN called DelphiMTSDemo needs to
be set up to connect to DemoData.mdb. }
strConnectionString := 'DSN=DelphiMTSDemo';
Data := CoRecordset.Create;
try
Data.CursorLocation := adUseClient;
Data.Open('SELECT * FROM Customers',
StrConnectionString, adOpenKeyset,
adLockBatchOptimistic, adCmdText);
// Disconnect from the database.
Data.Set_ActiveConnection(nil);
Result := Data;
finally
// Release the reference for good measure.
Data := nil;
end;
end;

Figure 10: Using an ADO COM object to retrieve a disconnected


recordset and return it to the client.
function TMTSDemo2._GetData: OleVariant;
var
obj : IMTSDemo1;
begin
OLECheck(ObjectContext.CreateInstance(
CLASS_MTSDemo1, IMTSDemo1, obj));
try
Result := obj.GetData;
finally
obj := nil;
end;
end;

Figure 11: Using the OLECheck function to raise any errors.

Because of this need to ensure that SetAbort or SetComplete is only


called once, and because this method may be called by another
method in this component, always put the main code for each
method in an internal routine, and call this from the external one.
Use an underscore to prefix the internal routine name to identify
it, as shown in Figure 9.
For this method, a typical MTS component method uses an ADO
COM object directly to retrieve a disconnected recordset and
return it to the client, as shown in Figure 10. There would usually
be a corresponding method to accept an edited recordset back
from the client and apply the updates to the database.
Because MTS is pooling database connections, its important to
open the database connection as late as possible, and to release it
as soon as possible, to reduce the likelihood of other components
needing to create a new connection because there are none available in the pool.

Calling Other MTS Components

One of the design goals of MTS applications is to have a number


of simple components working together to create a more complex
system. This has the usual benefits of easier development and
greater reusability. Is there anything special that needs to be done
to call another MTS component from our component? Under
MTS there is. If the second component is to participate in the

Distributed Delphi
same transaction as the first component, MTS must be told.
This is done through the context object. Unfortunately this is
one aspect of the context object that isnt encapsulated in the
TMtsAutoObject class.
The object context interface has a CreateInstance method that
allows us to request the creation of another MTS object. This
will then be enlisted within the same transaction, if its transaction
model was marked Requires a transaction or Supports transactions. If
its transaction model is marked Requires a transaction, it will always
have a separate transaction.
Fortunately, TMtsAutoObject has a protected property,
ObjectContext, that we can access to get the interface to the
context object. The following statement creates an instance of
the MTSDemo1 component:
ObjectContext.CreateInstance(
CLASS_MTSDemo1, IMTSDemo1, obj);

CreateInstance takes as its parameters the class ID (GUID) of the class


to be created, the interface to be returned, and the variable to hold
the interface. Because this is a direct COM call, we also need to wrap
it in the OLECheck function that will raise a Delphi exception if
any errors occur. For example, the code shown in Figure 11 creates a
second MTS object (MTSDemo2) with a GetData method, that then
calls MTSDemo1s GetData method.

Initialization
If theres any code that needs to be run to initialize an MTS component,
rather than placing it in the Delphi object constructor or the usual COM
Initialize method, it should be placed in the OnActivate method for the
MTS component. This is a protected method in TMtsAutoObject that
should be overridden in our component. This method is called by MTS
after the component has been created in preparation for a method call,
but before the actual call. Its always called before the method, even if the
component has been retrieved from the object pool (in COM+). Object
pooling is the reason for this; pooled objects should behave the same
whether they have just been created, or retrieved from the pool. This
makes it important that theyre initialized in the same way. Theres also a
Deactivate method where cleanup code can be placed.
Its important that exceptions are not allowed to escape from these
two methods. MTS doesnt expect initialization to fail, and so
does not handle this event very well. The client will just see a
catastrophic failure, which is neither informative nor reassuring.
This means that a try..except block must wrap the code in these
methods. Also, in the case of the OnActivate method, if the error
is to be handled, information about it must be recorded in the
component, and the method itself must check this and raise an
exception if required.

Object Pooling
If were developing components for COM+ and want to take advantage of object pooling, there are a number of steps we need to take.
The first is to use the Both threading model. The second is to override
the TMtsAutoObject.CanBePooled method to return True. By default
this switches pooling off.
Third, the object must be initialized to the same state, whether it has
just been created or retrieved from the pool. The last, and possibly
most difficult step, is to make sure the object is thread-safe, but that
topic is beyond the scope of this article.
23 May 2001 Delphi Informant Magazine

Figure 12 (Top): Running the MTS Objects wizard.


Figure 13 (Bottom): Select a package, or name a new package.

Installing the Component into MTS


Now that we have an MTS component, we need to test it. The
first step is to install it into MTS. Again, this process is simplified
by Delphi. Select Run | Install MTS Objects to run the Install MTS
Objects wizard (see Figure 12). The wizard prompts for the name
of the MTS package (COM+ application) into which to install
the component.
Check the components to install, and a dialog box will be displayed allowing you to select an existing package, or to specify
the name of a new package (see Figure 13). Only the name is
required, although you can also enter a description of the package.
The wizard does the rest.

MTS Packages
What is a package or COM+ application on Windows 2000? MTS
components are in-process DLLs, so they need an application
process in which to run. For MTS this is mtx.exe, and for
COM+ this is dllhost.exe. If all components ran in the same
process space, any fatal exceptions would cause every application
running on MTS to fail. MTS limits the damage that this would
cause by grouping components into packages, each running as a
separate process.
A package can contain components from more than one DLL,
and a DLL can contain components that are in several packages.
The only restriction is that each component can only be in one
package. Besides providing process isolation, under MTS, packages
also define security boundaries. Calls into a package are checked to

Distributed Delphi
ensure the caller has sufficient privilege. Calls within a package are
not checked. In COM+, security checking can be done at a much
finer level, down to individual methods if required.

Debugging
We now have our application installed in MTS. How do we run and
debug it? First we need a client application that will call the component.
I always create a test bed application for each component (or group of
components). This greatly simplifies debugging and testing.
In the case of the MTSDemo1 component, this is a simple form
with the following components: DBGrid, DataSource, ADODataset, and Button. (The project was saved as MTSDemo1Lib.) The
code for the button can simply be:
procedure TForm1.Button1Click(Sender: TObject);
var
obj : Variant;
begin
obj := CreateOleObject('MTSDemo1Lib.MTSDemo1');
ADOTable1.Recordset :=
IUnknown(obj.GetData) as _Recordset;
end;

To debug the MTS component, we need to specify the host


application for the component, and the parameters for it. The
host application for MTS is mtx.exe in the \SYSTEM32 directory.
For COM+, its dllhost.exe in the same directory. The parameters
differ for the two:
 For MTS: /p:"<package name>"
 For COM+: /ProcessID:{<Class ID>}
The component can then be run from Delphi, and debugged
whenever any client calls it. To debug the component and test
bed simultaneously, use Project | Build All Projects to compile both.
Then run the component, followed by the test bed.

Deployment
Once the component has been fully tested, were ready to deploy
it. This is done using the Transaction Server Explorer for MTS, or
Component Services for COM+. These two tools are similar and
sport an Explorer-style interface, as shown in Figure 14.
MTS simplifies installation by allowing packages to be exported. To
do this, select the package in the left-hand tree view, right-click on it,
and select Export. This creates a package file and a copy of the DLL
that can be installed on other servers, complete with security settings
if required. It also creates a client installation program that can be run
on each client machine to connect it to this server.
This client installation program registers the COM component on
the client computer, greatly simplifying client installation. (Note: The
client install registers the server from which the export was run, so usually the package will be exported from the development environment,
and installed on the production machine first. Then its exported from
the production machine to create the client installation program.)

24 May 2001 Delphi Informant Magazine

Figure 14: Explorer-style interface of Component Services for


COM+.

Once the package has been installed on the production system, the
transaction models for the components can be adjusted, and the
security set up. The MTS security model uses roles to group NT
users and groups. These roles are then granted access to components,
or even individual methods in COM+. This provides a secure
and flexible environment that can be controlled without changing
the components. The TMtsAutoObject methods, IsCallerInRole and
IsSecurityEnabled, can be used to provide finer control within the
component, but this is rarely necessary.
The creation of roles, the assignment of users and groups to them, and
the allocation of roles to components is done using the Transaction Server
Explorer, which makes extensive use of context menus.

Conclusion
Writing MTS components is straightforward with Delphi, providing a few simple precautions are heeded, and great care is taken
to produce robust, high-quality components. MTS provides a
number of services, from database connection pooling to security
checking. This can greatly simplify the task of creating a robust,
scalable middle-tier application to work with thin clients and
Web sites.
This article is based on a paper that was originally presented at the 11th
Annual Borland Conference, and is used with permission.
The files referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAY\
DI200105MM.

Malcolm is a senior consultant with Dunstan Thomas Limited and has many years
experience designing and implementing client/server and distributed database
applications using both Microsoft and Borland technologies. As well as being a
Borland Certified Delphi Consultant, he is a Microsoft Certified Software Developer
(MCSD). Malcolm has consulted for clients throughout Europe and is currently
focused on developing Web-based systems using Delphi, MTS, and XML. He spends
his time away from computers being jumped on by the kids.

Greater Delphi
Excel Automation / OLAP / PivotTables / Database / Delphi 5

By Alex Fedorov and Natalia Elmanova

Delphi, Excel, and OLAP


Analyzing Your Data in Multiple Dimensions

n a set of articles devoted to using Microsoft Office Web Components (in the December
2000 and January 2001 issues), you learned how to provide Delphi applications
with simple OLAP (online analytical processing) capabilities by using the PivotTable List
component to pivot, filter, and summarize information from various data sources and
present it in meaningful ways.
In this article, well continue to discuss implementing OLAP in Delphi applications, and outline
some advanced features of the Microsoft Excel PivotTable Services that are absent in the PivotTable
List component, but are available through Excel
Automation.

PivotTable Services Revisited


As described in the January article, Easy Pivot
Tables, an Excel PivotTable is an interactive table
usually used to summarize or statistically analyze
large amounts of fact data. The data can come from
a variety of sources, including an Excel range or
database query.
When you select Data | PivotTable and PivotChart
Report in Excel, the PivotTable and PivotChart
Wizards are displayed, respectively. You should
select a data source with this wizard, as well as
tables and fields to hold the data for the PivotTable (or create a complex query using Microsoft Query). Then, visually define which of the
selected fields will form the row, column, and
filter (or page) fields, and which should be summarized, as shown in Figure 1.

Figure 1: Creating a PivotTable manually in Excel.


25 May 2001 Delphi Informant Magazine

Opposite the PivotTable List component, these


fields (also called data fields) wont be shown
in the PivotTable; only their summaries will be
downloaded. In the case of using non-numeric
fields as data fields, you can obtain their counts.
However, you can download the necessary details
by selecting the Group and Online | Show Detail
pop-up menu of a particular cell; this results
in placing detailed data on the newly created
worksheet.

Greater Delphi
Application

Legend:
Object

WorkBooks
(WorkBook)

WorkSheets
(WorkSheet)

PivotTables
(PivotTable)

PivotFields
(PivotField)

Columns
(Column)

CubeFields
(CubeField)

Charts
(Chart)

Collections

PivotCaches
(PivotCache)

Chart

Chart Area

const
// Path to the Northwind.mdb file.
NW_Path = 'C:\Data\';
PC.CommandType := xlCmdSql;
PC.CommandText :=
'SELECT Invoices.Country, Invoices.City,' +
' Invoices.Customers.CompanyName,' +
' Invoices.ProductName, Invoices.Salesperson,' +
' Invoices.Shippers.CompanyName,' +
' Invoices.ExtendedPrice' +
' FROM "'+NW_Path+'Northwind".Invoices Invoices' +
' WHERE Invoices.Country = ''France'''+
' OR Invoices.Country = ''Germany''';
// Create a PivotTable.
PC.CreatePivotTable(
WB.Worksheets[1].Cells[3,1], 'PivotTable1');
PT := WB.Worksheets[1].PivotTables('PivotTable1');

Figure 2: Portion of Excel object model used for PivotTable Services.


Figure 4: Supplying values for CommandType and CommandText.
Property
Connection

CommandType

CommandText

Description
The string in the following form: <Data
Source Type>;<ADO Connecton String>,
where Data Source Type can be ODBC, OLE
DB, URL, or TEXT, depending on how Excel
should connect to the data source.
xlCmdCube OLAP cube
xlCmdSql SQL query
xlCmdTable Database table
xlCmdDefault Query text understood by
OLE DB provider
The text of a database query, or an OLAP
cube/table name

Figure 3: PivotCache object properties.

Excel PivotTable Services allows you to create PivotTables (or


crosstabs) based on Excel ranges and database queries, as well as
OLAP cubes created with Microsoft SQL Server or other tools
available through OLE DB or ADO. In the last case, the PivotTable and PivotChart Wizard doesnt allow you to move available
fields to any area all possible axes, filter fields, and data fields
are already defined. Keep in mind that when a PivotTable is based
on an OLAP cube, retrieving detail data is impossible due to the
nature of an OLAP cube. It should also be noted that Excel can
create a local OLAP cube stored in a .cub file.
An OLAP cube is a data structure that contains OLAP data,
i.e. dimension and data fields. Data in dimension fields can be
organized into hierarchies with several levels of detail, while data
fields contain values that should be tracked in a relational database.
When you create an OLAP cube from a database table or query, you
convert the flat set of records into this kind of structured hierarchy.
In most cases, OLAP cubes are created in multidimensional databases
maintained by OLAP servers, e.g. Microsoft SQL Server. However,
sometimes its convenient to create local multidimensional file storage
where the data is organized in the same way. Excel with Microsoft
PivotTable Services provides such a possibility. It allows you to create
a portable storage of OLAP data in a single file. Here, you dont
need an OLAP server to create and use this type of OLAP cube.
When selecting the option for creating a local OLAP cube after defining a database query, youre presented with the OLAP Cube Wizard.
It helps the user create a cube interactively by filling in a sequence of
dialogs. However, in spite of being a well-designed wizard, using PivotTable Services still remains complex for most Excel users. It requires
26 May 2001 Delphi Informant Magazine

Figure 5: The PivotTable inside the OLE container.

some advanced knowledge of appropriate database structure and field


names, as well as OLAP cube dimensions, hierarchies, and their levels.
In many cases, the best way to provide users with OLAP possibilities,
and guard them against such complexity, is to automate Excel in
your application. Well do just this, by programmatically creating
a PivotTable and local OLAP cube. Well read existing local cubes
and SQL Server OLAP cubes, make charts based on PivotTables and
OLAP cubes, and save them as bitmap files.

Using Excel Objects


Before creating applications using PivotTable Services Automation, you
need to know which Excel objects to use. The part of the Excel object
model used for manipulating PivotTable Services is shown in Figure 2.
The main object used for creating PivotTables is PivotCache. It represents the memory cache for a particular PivotTable (please note
that theres no similar objects in the PivotTable List component).
This object is available through the PivotCaches collection that
results from calling the PivotCaches method of the Excel Workbook
object. To create a new PivotCache object, you need to call the
Add method of the PivotCaches collection. This method accepts two
parameters, with the first indicating the type of pivot source:
 xlConsolidation multiple consolidation ranges
 xlDatabase Excel list or database
 xlExternal data from another application
 xlPivotTable same data source as another PivotTable

Greater Delphi
CREATE CUBE [OCWCube]
(
DIMENSION <Dimension Name> [TYPE <Dimension Type>]
LEVEL <Level Name>[TYPE <Level Type>],
[ LEVEL <Level Name> [TYPE <Level Type>]...],
[ [DIMENSION <Dimension Name> [TYPE <Dimension Type>]
LEVEL <Level Name>[TYPE <Level Type>,
[ LEVEL <Level Name> >[TYPE <Level Type>...],...],
MEASURE <Measure Name> FUNCTION <Function Name>,
[MEASURE <Measure Name> FUNCTION <Function Name>,...]

Figure 6: CREATE CUBE statement syntax.


InsertInto=INSERT INTO OCWCube
(
<Dimension Name>.<Level Name>,
[<Dimension Name>.<Level Name>,...]
<Measure Name>,
[<Measure Name>,... ]
)
OPTIONS ATTEMPT_ANALYSIS

Figure 7: INSERT statement syntax.

// Add the cube name into the connection parameters.


ConnStr :=
'OLEDB;Provider=MSOLAP;Initial Catalog=[OCWCube]; ' +
'Data Source=' + SaveDialog1.FileName + '; ' +
// The CREATE CUBE statement.
'CreateCube=CREATE CUBE [OCWCube] (' +
'DIMENSION [Country], ' +
'LEVEL [All] TYPE ALL, LEVEL [Country], LEVEL [City], ' +
'LEVEL [CompanyName], DIMENSION [CompanyName1], ' +
'LEVEL [All] TYPE ALL, LEVEL [CompanyName1],' +
'DIMENSION [Salesperson], ' +
'LEVEL [All] TYPE ALL, LEVEL [Salesperson], ' +
'DIMENSION [ProductName], ' +
'LEVEL [All] TYPE ALL, LEVEL [ProductName], ' +
'DIMENSION [OrderDate] TYPE TIME, ' +
'LEVEL [All] TYPE ALL, LEVEL [Year] TYPE YEAR, ' +
'LEVEL [Month] TYPE MONTH, ' +
'MEASURE [ExtendedPrice] FUNCTION SUM); ' +
// The INSERT statement.
'InsertInto=INSERT INTO OCWCube([Country].[Country], ' +
'[City], [CompanyName], [ProductName].[ProductName], ' +
'[CompanyName1].[CompanyName1], ' +
'[Salesperson].[Salesperson], '[OrderDate], ' +
'[ExtendedPrice], [CompanyName].[CompanyName]) ' +
'OPTIONS ATTEMPT_ANALYSIS '+
// The SELECT statement.
'SELECT Invoices.Country, Invoices.City, ' +
'Invoices.Customers.CompanyName, Invoices.ProductName,' +
'Invoices.Shippers.CompanyName, Invoices.Salesperson, ' +
'Invoices.OrderDate,Invoices.ExtendedPrice ' +
'FROM Invoices; Source_DSN="DSN='+ODBC_DSN+'"';

The CreatePivotTable method accepts two parameters. The first specifies the upper-left cell of the worksheet where you place the PivotTable, and the second specifies the name of the PivotTable object:
PC.CreatePivotTable(
WB.Worksheets[1].Cells[3,1], 'PivotTable1');

You can produce PivotTables or read OLAP cubes by creating a


new PivotCache object, defining its Connection, CommandType,
and CommandText properties, and calling the CreatePivotTable
method of this object.

Programming PivotTable Services


For the examples in this article, the OleContainer component (from the
System page of the Delphi Component palette) thats usually employed as a
visual container for OLE documents in Delphi applications is used. In this
case, you can show the Excel window directly on a Delphi form. You can
also decide if users should have access to the Excel menus and toolbars. (If
you do, just place the Panel and MainMenu components on the form.)
First, create an instance of the Excel application inside the OLE
container:
var
WB: Variant; // Workbook.
PC: Variant; // PivotCache.
PT: Variant; // PivotTable.
...
// Create an object in the OLE Container.
OleContainer1.CreateObject('Excel.Sheet', False);
WB := OleContainer1.OleObject;

Next, add a new PivotCache object:


// Add a PivotCache.
PC := WB.PivotCaches.Add(xlExternal);

Now you can define the Connection, CommandType, and CommandText


properties of the PivotCache object and create a PivotTable. These
properties depend on the data source type, and the following sections
describe in detail their values for each case.

Based on a Database Query


Create a PivotTable based on a database query, using the ODBC data
source. In this case, its named NW:
const
// ODBC data source name for creating a PivotTable.
ODBC_DSN = 'NW';
...
PC.Connection := 'ODBC;DSN='+ODBC_DSN;

Figure 8: Creating a local cube.

The second parameter of this method is optional: it can be an Excel


range, or combination of an ADO connection string and a data
source name. This parameter is not required when the first parameter
is xlExternal, and is not significant for these examples, because youll
use external data sources only.
The key method of the PivotCache object is CreatePivotTable. It
creates a PivotTable object ands adds it to the PivotTables collection
of the Worksheet object, based on the data source defined in the
properties of the PivotCache object (see Figure 3).
27 May 2001 Delphi Informant Magazine

Because the PivotTable should be based on a database query, be sure


to place its text into the CommandText property of the PivotCache
object (see Figure 4).
Now, you have two ways to continue the job. You can let users drag
and drop fields into the PivotTable manually, or you can do this
programmatically.
Heres how the second way can be implemented. For any field in
the database query on which the PivotTable is based, you can set
the Orientation property of the appropriate PivotField object thats
accessible through the PivotFields collection:

Greater Delphi
This results in a PivotTable with appropriate row, column, page, and
data fields inside the OLE container, as shown in Figure 5. By following this example, you programmatically created a PivotTable based on
a database query. In the next example, youll create a local OLAP cube
based on such a query.

Creating and Viewing a Local OLAP Cube


To create and view a local OLAP cube based on a database query,
create a new PivotCache object using the Add method of the
PivotCaches collection:
PC := WB.PivotCaches.Add(xlExternal);

This pivot cache will store data from an OLAP cube that you create. The
next step is to build a connection string to create and access this cube.
This string should consist of several parts. The first part defines the data
source that will point to the future cube: OLEDB; Provider=MSOLAP;
Initial Catalog=[OCWCube]; Data Source=c:\data\northwind.mdb.
Figure 9: The PivotTable based on the newly created OLAP cube.

Other parts of this connection string are responsible for creating


the cube by programming PivotTable Services. One is the CREATE
CUBE statement that defines cube dimensions and their levels, as well
as cube measures (see Figure 6). Another is the INSERT statement for
adding rows to the cube (see Figure 7).
Finally, the SQL SELECT query returns the fact data used to create a
cube. See Figure 8 for all the code related to creating a local cube. (Note:
You can find details regarding the syntax of the CREATE CUBE and
INSERT INTO statements in SQL Server 2000 Books Online, in the
Analysis Services programming section of the MSDN library.)
The next step is to set the Connection, CommandType, and
CommandText properties of the just created PivotCache object:
// Add cube name to connection parameters.
PC.Connection := Connstr;
PC.CommandType := xlCmdCube;

Figure 10: The pivot chart in the OLE container.

PT.PivotFields('ProductName').Orientation := xlPageField;
PT.PivotFields('SalesPerson').Orientation := xlColumnField;
PT.PivotFields('ExtendedPrice').Orientation := xlDataField;

The possible values of the Orientation property are:


 xlRowField place in row field area
 xlColumnField place in column field area
 xlPageField place in page (filter) field area
 xlDataField place in the data field area
 xlHidden field isnt used in the PivotTable (default value)
If the PivotTable has more than one row, column, page, or data
field, you can arrange them in the appropriate order (for example,
corresponding to levels of a time, geographical, or other hierarchy)
using the Position property of the PivotField object:
// Select row, column, page fields for PivotTable.
PT.PivotFields('Country').Orientation := xlRowField;
// Country is at first level of a geographical hierarchy.
PT.PivotFields('Country').Position := 1;
PT.PivotFields('City').Orientation := xlRowField;
// City is at second level of a geographical hierarchy.
PT.PivotFields('City').Position := 2;

28 May 2001 Delphi Informant Magazine

PC.CommandText := 'OCWCube';

Now, define the fields to be used for creating row, column, page, and
data fields of the PivotTable. In this case, use the CubeFields collection
of the PivotTable object. Please note that in Delphi you cant access the
CubeFields collection members by their names; access is only available to
them through their indexes:
PT.CubeFields[2].Orientation :=
xlRowField;
// Country.
PT.CubeFields[5].Orientation :=
xlColumnField; // Salesperson.
PT.CubeFields[4].Orientation :=
xlPageField;
// ProductName.
PT.CubeFields[6].Orientation :=
xlDataField;
// ExtendedPrice.

The resulting PivotTable is shown in Figure 9. In this example,


you can only use the cubes dimensions and its levels as
row, column, or filter fields, and only the cubes measures (in
this case, the SUM of the ExtendedPrice field) as data fields.
These restrictions result from the absence of fact data in the
OLAP cubes.

Reading Local and Server OLAP Cubes


Reading existing OLAP cubes is simpler than creating them programmatically; you dont need to define cube dimensions, levels, and mea-

Greater Delphi
var
BMP: TBitmap;
...
if SaveDialog2.Execute then begin
// Copy the Chart area to Clipboard.
WB.Charts[1].ChartArea.Copy;
// Create a TBitmap object.
BMP:=TBitmap.Create;
// Load Clipboard content into it.
BMP.LoadFromClipboardFormat(cf_BitMap,
ClipBoard.GetAsHandle(cf_Bitmap),0);
// Save bitmap to the specified file.
BMP.SaveToFile(SaveDialog2.FileName);
// Free allocated resources.
BMP.Free;
end;

Figure 11: Saving a pivot chart as a bitmap.

sures, or specify the data source on which the cube is based. To open a
local cube, you need to specify the ADO connection string to access the
cube that contains the OLE DB provider name (in this case, its the OLE
DB provider for OLAP Services), and the name of the .cub file:
PC.Connection := 'OLEDB;Provider=MSOLAP; ' +
'Initial Catalog=[OCWCube]; Data Source=C:\MyCube.cub';
PC.CommandType := xlCmdCube;
PC.CommandText := 'OCWCube';

As explained earlier, showing details in PivotTables based on OLAP


cubes is impossible.

Creating and Saving Pivot Charts


Charting PivotTables is simple; the only thing you need to do is add
an item to the Chart collection of the Workbook object, and specify
the type of the chart:
WB.Charts.Add;
WB.Charts[1].ChartType := xlColumnClustered;

The result of charting the PivotTable is shown in Figure 10.


Why havent you defined an Excel range to create a chart? The reason
is that if the Excel PivotTable is currently active, it becomes a data
source for the new chart by default. However, usually the range for
the chart should be selected.

Saving Pivot Charts as Bitmaps


If youd like to save the pivot chart in a bitmap file, copy the chart to
the Clipboard using the Copy method of the ChartArea object. Then,
create a TBitmap object, load the Clipboard content into this object,
and save it to a file (see Figure 11).

Conclusion

PC.Connection := 'OLEDB; Provider=MSOLAP; ' +


'Data Source=maindesk;Initial Catalog=FoodMart; '
PC.CommandType := xlCmdCube;
PC.CommandText := 'Sales';

In this article, youve seen how to use PivotTable Services in


Delphi applications through Excel Automation. Now you know
how to create a PivotTable based on a database query, create and
read a local OLAP Cube, and define row, column, page, and
data fields for PivotTables. Youve also studied how to produce a
PivotTable based on OLAP cubes created with SQL Server OLAP
Services or SQL Server 2000 Analysis Services. Last, but not
least, youve learned how to make a pivot chart and save it to a
bitmap file.

If you need to open a cube created with SQL Server 2000 Analysis
Services, you need to specify the appropriate connection string:

The files referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAY\
DI200105AF.

To open an OLAP cube created with SQL Server 7.0 OLAP Services,
you need to specify the ADO connection string to access the OLAP
server and the OLAP cube name:

PC.Connection := 'OLEDB;Provider=MSOLAP.2;' +
'Data Source=maindesk;Initial Catalog=FoodMart 2000;'
PC.CommandType := xlCmdCube;
PC.CommandText := 'OCWCube';

The remaining steps can be the same as in the previous case;


you can allow a user to select row, column, page, and data fields
manually, or have your application do this job programmatically:
PT.CubeFields[5].Orientation :=
xlColumnField; // Product family.
PT.CubeFields[2].Orientation :=
xlRowField;
// Education level.
PT.CubeFields[3].Orientation :=
xlPageField;
// Gender.
PT.CubeFields[4].Orientation :=
xlPageField;
// Marital status.
PT.CubeFields[14].Orientation :=
xlDataField;
// Sales average.

29 May 2001 Delphi Informant Magazine

Alex Fedorov is a Chief Technology Officer for Netface SA, based in Lausanne,
Switzerland (http://www.netface.ch). Hes one of the co-authors of Professional
Active Server Pages 2.0 [Wrox Press, 1998] and ASP 2.0 Programmers Reference [Wrox Press, 1999]. Natalia Elmanova, Ph.D., is an executive editor for
ComputerPress magazine published in Moscow, Russia (http://www.compress.ru).
She was a contributing author to the 10th and 11th Annual Inprise & Borland.com
Conferences. Natalia and Alex are authors of Advanced Delphi Developers Guide
to ADO [Wordware Publishing, 2000], and several programming books written in
Russian. You can visit their Web site at http: //d5ado.homepage.com.

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

By Eli Bar-Yosef

Getting Started with OpenGL


Part I: Vertices, Matrices, Color, and Transformations

f youve ever thought about programming a 3D-flight simulator, or wanted to embed a


cool, 3D component in your application, this two-part series is for you.

The computing power needed for 3D graphics


used to be out of reach of most developers let
alone users. Now computer hardware is so powerful and reasonably priced that most users can afford
to buy systems capable of running 3D graphics.
Consequently, we developers can supply eye-popping visuals to our applications.
If youve ever played Quake or Unreal, you probably
wondered how they did it. The general answer is
OpenGL, and thats exactly what this series will demonstrate. Part one of this series will introduce you to
the theory of 3D, coloring, and transformation.

+Y
-Z

+X

-X

+Z
-Y
Figure 1: 3D coordinate system.

30 May 2001 Delphi Informant Magazine

OpenGL
OpenGL (Open Graphics Library) is a software
interface to graphics hardware that helps to
program 3D applications. The OpenGL API
includes about 300 commands with which you
can construct a 3D object from small geometric
primitives such as dots, lines, triangles, etc., as
well as modeling objects, pouring light, adding
texture, etc.
Initiated by Silicon Graphics (SGI), OpenGL is
supported by many large companies such as Microsoft, Compaq, and HP and can be used on most
popular operating systems such as Windows, Mac
OS, Linux, and others. Currently there are several
software interfaces to graphic hardware. DirectX,
developed by Microsoft, and OpenGL are the most
well known.
In my humble opinion, DirectX has more drawbacks than OpenGL. First, to use DirectX you
must be COM-savvy (admittedly not a problem
for most Delphi developers). Second, you cant port
your applications to other operating systems; your
application can only run on Windows. Third, I
find the DirectX API a little cumbersome compared to the OpenGL API.
Now that you know what OpenGL is and what
you can achieve by using it, lets look at 3D
programming concepts in general, then OpenGL
syntax and commands.

Sound+Vision
Vertices and Matrices
Our world is three dimensional: Every object can be represented on
three axes: the x, y, and z axes. So when you build your 3D objects,
you should always think of your screen as a coordinate system that
has an x, y, and z axis.

vides a series of geometric primitives, meaning it provides only


the most basic shapes, such as points, lines, triangles, and quads.
Mix them with a little creativity and trigonometry and you can
build almost any kind of object. For instance, you can construct
spheres, cubes, circles, NURBS curves, and so on.

As you can see in Figure 1, on the right side of the screen you have the
positive x axis, on the left the negative x axis. Up is the positive y axis,
and down is the negative y axis. You can also see that when you move
inside the screen you have the negative z axis, and out of the screen,
you have the positive z axis.

OpenGL objects are represented by vertices. Therefore, when you


want to draw a simple 2D line (see Figure 3), its necessary to
specify two vertices that will represent the line, i.e. (2,2,0) and
(-2,-2,0). Thats the way to represent the line mathematically, but
if you want OpenGL to understand it, youll have to write:

In the 3D world we define vertices. Vertices are points that are


used to describe an object. Vertices have no direction, in contrast
to vectors which do. A vector is the difference between two
vertices. In some situations, you can also represent one vertex by
a vector. This happens when the second vertex is on the origin
(0,0,0).

glBegin(GL_LINES);
glVertex3f( 2.0, 2.0, 0.0);
glVertex3f(-2.0, -2.0, 0.0);
glEnd();

A matrix in its most abstract


sense is a simple way to represent
a complex system of equations.
The 3D world poses some pretty
complicated equations, so the
best way to solve them is to use
matrices. Before we continue, its
important for you to understand
what the values of the matrices
represent. In 3D programming
we use 4x4 matrices, as you will
see in the rest of the article.
As shown in Figure 2, the 4x4
matrix contains four vectors that
represent the 3D world.

Figure 2: The 4x4 OpenGL


matrix.

The vectors in the first three rows are unit vectors that define directions. More specifically, the first row represents the 3D coordinates
space of the local x axis, the second on the y, and the third on the z.
The three rows collectively represent the rotation of the object. The
last row represents the objects position (translation) in the 3D space.
Theres another thing you must keep in mind when you deal
with matrices: Matrix multiplication is not commutative. In other
words, the order of the operations you perform on the matrix is
important. This concept will be clearer in a later section when we
discuss transformation.
Now that you understand some of the 3D programming concepts,
lets look at the OpenGL
API. (For in-depth coverage
2, 2 of 3D concepts, check the
links at the end of this article. Some of the links are for
sites containing numerous
articles regarding vectors,
matrices, collision-detection
algorithms, Artificial Intelligence (AI), and so on.)

-2, -2

Figure 3: A line.
31 May 2001 Delphi Informant Magazine

Objects
Objects are one of the
basic building blocks of
your scene. OpenGL pro-

As you can see from this code, the drawing starts with
glBegin(GL_LINES). This means that we start our drawing and specify which primitive shape we would like to draw. In this case, we
stated that we wanted to draw a line by using GL_LINES. Then we
specified to OpenGL that we want to draw two vertices. That has
been done by using the function glVertex3f(x, y, z).
The 3 in this function means were using a function that needs the
three parameters x, y, and z. The f means well use a function that will
get a floating point number as an argument. If you check the OpenGL
API documentation, youll notice several functions that are similar to
this function, but each has different parameters. For instance, when you
want to draw a 2D line, you would normally use glVertex2i(x, y). The
2 indicates this function takes only two parameters, x and y; the suffix
i means you must supply the parameters as integers.
Then, to conclude the drawing, its necessary to call glEnd. Now you
know how to draw a simple line. Next, lets draw a simple polygon.
A polygon is a shape built from several lines or dots, and closed in a
specific area. When you build a polygon, there are two rules:
 the lines cannot intersect
 the polygon must be convex
This code describes a simple polygon with four vertices:
glBegin(GL_POLYGON);
glVertex3f(-1.0, -1.0,
glVertex3f(1.0, -1.0,
glVertex3f(0.5,
0.5,
glVertex3f(-0.5, 0.5,
glEnd ();

0.0);
0.0);
0.0);
0.0);

Similar to the previous example, we first told OpenGL that we


wanted to start our drawing and specified the shape by using the
glBegin(GL_POLYGON) command. Next we have to specify the
vertex of the polygon by using glVertex3f(x, y, z).
OpenGL supports some other geometric shapes. Unfortunately, it
isnt possible to cover all of them in this series, but you can research
them yourself. The rest work similarly to those already described.

Colors
When youre drawing an object using OpenGL, you may want
to specify its color. OpenGL has a special function for specifying
the object color, named glColor3*(...), and it takes red, green, and
blue (RGB) as arguments. The asterisk means this function has

Sound+Vision
+Y

+Y

+Y
+Y
+Y

-X

+X

-X

+X

-X

+X

+X
-X

-Y

-Y

-Y

-Y

-Y

1. Initial state

2. Translate on the X axis

3. Rotate 45 degrees
around the Z axis

Figure 4: Results of translating before rotating.

several versions. You can specify byte values using glColor3b(...),


the arguments for this version must be between 0 and 255; 255
has the highest intensity, and 0 has the lowest. You can also use
glColor3f(...); this function takes three float arguments, each one
between 0.0 and 1.0.
When you want to set the color of the object, you need to specify the
color in each vertex. Once you specify the color, it will remain in effect
until you call glColor3*(...) with a different color. As mentioned, you
need to specify the color using RGB. With combinations, you can get
any color that you want. For instance, when you want a white vertex,
use glColor3f(1.0, 1.0, 1.0); for yellow, use glColor3f(1.0, 1.0, 0.0).
Lets use the code we wrote in the previous example, and pour on
the color. The following will add red, yellow, blue, and green, making
each vertex a different color:
glBegin(GL_POLYGON);
glColor3f( 1.0, 0.0,
glVertex3f(-1.0, -1.0,
glColor3f( 1.0, 1.0,
glVertex3f( 1.0, -1.0,
glColor3f(
0.,
0.,
glVertex3f( 0.5, 0.5,
glColor3f( 0.0, 1.0,
glVertex3f(-0.5, 0.5,
glEnd();

0.0);
0.0);
0.0)
0.0);
1.0);
0.0);
0.0)
0.0);

// red
// yellow
// blue
// green

Transformations
Now you need to set the position and orientation of the object youve
created. In other words, its time to deal with transformations. Think
of the transformation process on the computer as you would a camera:
You need to set the camera properties of orientation, position, and lens
type. When you want to view the scene you need to specify:
 Viewing
 Modeling
 Projection
Each of these transformations is represented by a 4x4 matrix. To
achieve the desired effect on your scene, you will have to multiply
your current matrix by the matrix youve constructed. The formula
is V = MV, where M represents your transformation matrix, and V
is the current matrix.
When you implement the transformation on your 3D scene, you
must implement it in a specific order, or you wont get the desired
results. The order to use is Viewing, Projection, and Modeling.
32 May 2001 Delphi Informant Magazine

Before you change the transformation of your scene, you must tell
OpenGL by which matrix it should multiply your 4x4 matrix. You
can do that by using the procedure glMatrixMode(mode: GLenum). The
mode can be one of the three values: GL_ModelView, GL_Projection, or
GL_Texture. Where is the GL_View value? The Viewing transformation
is the same as the Modeling transformation. It will soon become clear.
Again, when youre dealing with the Viewing transformation, think of
it as positioning a camera on your object. Lets say you want to move
the camera two units to the right to change the view so it will show the
object from a 45-degree angle. To do this you can use the command:
procedure gluLookAt(eyex: GLdouble; eyey: GLdouble;
eyez: GLdouble; centerx: GLdouble; centery: Gldouble;
centerz: GLdouble; upx: GLdouble; upy: GLdouble;
upz: Gldouble);

This command can help you to build a 4x4 matrix for performing the
transformation. Notice in the previous code that glu* was used, not
gl*. Thats because this function is called from the GL Utilities (GLU)
library. This library has a nice collection of functions that might help
you accomplish some tasks more smoothly.
To use gluLookAt(...) you have to supply three groups in which each
group contains three points. The first is eye*(x, y, z), for the position
of the camera on the screen. The second is center*(x, y, z), for the
direction of the camera lens. The third is up*(x, y, z), which sets the
orientation of the camera.

Modeling Transformation
Modeling transformation is used to set your objects position and
orientation inside the scene. To achieve Modeling transformation,
OpenGL offers the use of three special Modeling transformation procedures: glTranslate*(x, y, z), glRotate*(Angle, x, y, z), and glScale*(x, y,
z). glTranslate*(x, y, z) lets you move your object any direction on the
screen. Lets say you want to move the polygon weve constructed three
moves to the right side of the screen, and 10 moves inside the screen.
The following code achieves this:
glMatrixMode(GL_ModelView)
glLoadIdentity();
glTranslatef(3.0, 0.0, -10.0);
glBegin(GL_POLYGON);
glVertex3f(-1.0, -1.0, 0.0);
glVertex3f( 1.0, -1.0, 0.0);
glVertex3f( 0.5, 0.5, 0.0);
glVertex3f(-0.5, 0.5, 0.0);
glEnd();

Sound+Vision
+Y

+Y

+Y
+Y

-X

+X

+X

+Y
+X

-X

+X
-X

-X

-X

-Y

+X

-Y

-Y

-Y

-Y

1. Initial state

2. Rotate 45 degrees
around the Z axis

3. Translate toward the X axis

Figure 5: Results of rotating before translating.

The previous code sets the current matrix to be the model view
matrix. Next we need to clear the model view matrix. This is
done by multiplying the current matrix by the identity matrix.
Then we call the glTranslatef(...) procedure to help us to construct
a matrix that will be implicitly multiplied by the model view
matrix. Eventually, this will affect the position of the object in
the scene.
glRotate*(angle, x, y, z) lets you rotate the object in a specific angle,
and around a specific axis. As shown previously, when you call
glRotate *(Angle, x, y, z), you actually construct a matrix that is
multiplied by the current matrix. For instance, if you want to
rotate an object 30 degrees around its x axis, use glRotatef(30.f,
1.0, 0.0, 0.0).
glScale*(x, y, z) lets you shrink, stretch, or reflect an object along
its axis. glScale(...) takes three arguments. The arguments construct
a matrix that will be multiplied by the current matrix, the model
view matrix. Lets say you want to shrink an object. You would use
glScalef(0.5, 0.5, 0.5) to make an object half its size.
Again, matrix multiplication isnt commutative, so its critical to pay
attention to the order in which you multiply your matrices, as Figures
4 and 5 demonstrate.

Projection Transformation
Projection transformation specifies how your object will be projected
on the screen. In addition, it helps OpenGL decide which parts of
your objects should be clipped from the final image. There are two
ways to tell OpenGL how to project objects to the screen: perspective
and orthographic projection.
In this article, only perspective projection will be discussed. Perspective means that your objects get smaller or bigger depending
on the distance of the objects. This effect occurs because the
perspective projection has the shape of a truncated pyramid called
a frustum. The OpenGL library contains the glFrustum procedure
to specify this:
procedure glFrustum(left: Double; right: Double;
bottom: Double; top: Double; near: Double; far: Double);

As you might imagine, the bottom and left, and top and right arguments
define the lower-left and upper-right corners of the frustum, respectively;
33 May 2001 Delphi Informant Magazine

and near and far set the depth of the clipping plane. Now that you know
how to use the glFrustum(...), lets look at another command named
gluPerspective(...), the counterpart of glFrustum. Its defined in the GLU
library and is somewhat easier to use than the glFrustum procedure:
procedure gluPerspective(fovy: Double;
aspect: Double, near: Double; far: Double);

The fovy argument specifies the field-of-view angle in the y direction. In


other words, it specifies how wide the viewers field of view is. The aspect
is the aspect ratio of the frustum, or window height/window width. The
near and far arguments have the same duties as in glFrustum(...).

Conclusion
Thats it for this month. Practice making 3D objects, coloring them,
and transforming them. And watch out for the order of matrix
multiplication! Next month, well explore how OpenGL handles
lighting, materials, texture, and more. Ill also make available some
Delphi classes for manipulating OpenGL, and an example application that shows them off. See you then.

Important Resources
Check out http://www.OpenGL.org; it contains lots of usable
information about 3D programming and a huge number of
great links to other 3D programming sites. Also look into
http://www.nvidia.com for dazzling demonstrations on 3D
programming using OpenGL.

Eli Bar-Yosef is a professional software developer in Israel. He mainly uses


C/C++ and Delphi and specializes in Windows internals, Communication, and
COM/DCOM. Currently he is working on several freelance projects, as well as
on a new and special project for which he is seeking funds. In his free time,
he enjoys programming real-time 3D demos and reading. If you have any comments, suggestions, or if you want information about his project, contact him at
eli_by@netvision.net.il.

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

By Bruno Sonnino

Kylix Form Tricks


Creating Non-rectangular Forms with the Qt API

any tips presented in my At Your Fingertips column use the Windows API and
cannot be ported to Kylix. This month, however, well convert some tips to Kylix
using the Qt API. This conversion will give you an added benefit; theyll be portable to
Windows when Delphi 6 is released (Borland engineers report that CLX and the Qt API
will be included in Delphi 6). And, its also possible theyll be portable when (tongue
planted firmly in cheek) Delphi for Solaris, Delphi for Mac, Delphi for Palm, and other
Delphi platforms are launched.

Non-rectangular Forms in Windows


In the March 2000 issue of Delphi Informant Magazine, Peter Morris explained how to use regions
to draw non-rectangular forms: You must create
a region and use the SetWindowRgn to apply the
region to the form. The code in Figure 1 illustrates
how to create a rounded window. Although simple,
this code is very powerful and will draw the
rounded window shown in Figure 2.
You can combine regions to obtain a complex
region and apply it to a form. (In this article,
youll also learn how to create a region from
a bitmap, and develop a TFormShaper component to do it.) But this code isnt portable: Kylix
and Linux dont have HRGN, CreateEllipticRgn,
SetWindowRgn, and so on. Of course, you could
use Wine libraries (http://www.winehq.com) to
var
Region : HRGN;
Size : Integer;
begin
// Region size will be the smallest dimension.
if Width > Height then
Size := Height
else
Size := Width;
// Create the region.
Region := CreateEllipticRgn((Width-Size) div 2,
(Height-Size) div 2, (Width+Size) div 2,
(Height+Size) div 2);
// Apply to the form.
SetWindowRgn(Handle, Region, True);
end;

Figure 1: This code creates a rounded window.

34 May 2001 Delphi Informant Magazine

develop your program, but what would you do


in Delphi for Palm Pilot? Okay; youre not interested in Palm development, but lets use a portable solution anyway.

Non-rectangular Forms with Qt API


CLX is based on the Qt API, a class library
developed by the Norwegian company Troll Tech
(http://www.trolltech.com). Its a C++ class API
and its classes arent compatible with Kylix, but
Borland engineers did a great job importing it
to Pascal.
Qt defines a class named QRegion (sounds familiar,
no?) to handle regions, much as Windows does.
You can create a new region from a rectangle, an
ellipse, a polygon, or a bitmap. After youve created
the region, you can combine it with other regions,
creating a complex one.
You can use the setMask method of the
QWidget class to limit the area that will
be painted to that region. The QWidget
class is the base class for all interface
objects. In Windows, the interface components
(buttons, forms, and listboxes) descend from
TWinControl, and have the Handle property
pointing to the window handle. In Kylix, these
components descend from TWidgetControl; their
Handle property is a QWidgetH, a pointer to a
QWidget instance.
You usually dont have to worry about these
details, but when you want to work directly with
the Qt API, you must get your hands dirty. To

At Your Fingertips

Figure 2: A rounded form in Windows.


Figure 5: A round form in Linux.
function QRegion_create(): QRegionH; overload; cdecl;
function QRegion_create(x: Integer; y: Integer; w: Integer;
h: Integer; p5: QRegionRegionType): QRegionH;
overload; cdecl;
function QRegion_create(p1: PRect; p2: QRegionRegionType):
QRegionH; overload; cdecl;
function QRegion_create(p1: PPointArray; winding: Boolean):
QRegionH; overload; cdecl;
function QRegion_create(p1: QRegionH): QRegionH;
overload; cdecl;
function QRegion_create(p1: QBitmapH): QRegionH;
overload; cdecl;

Figure 3: Qts overloaded region constructors.


procedure TForm1.FormCreate(Sender: TObject);
var
Region : QRegionH;
Size : Integer;
begin
// Region size will be the smallest dimension.
if Width > Height then
Size := Height
else
Size := Width;
// Create the region.
Region := QRegion_create((Width-Size) div 2,
(Height-Size) div 2, Size, Size,
QRegionRegionType_Ellipse);
// Apply to the form.
QWidget_setMask(Handle, Region);
end;

Figure 4: Creating a round window with the Qt API.

use any Qt API function, you must include the Qt unit in your
uses clause. Then use any of the overloaded constructors (see
Figure 3) to create a region using the Qt API.
The second and third constructors create an elliptic or a rectangular region, depending on the value passed in the last parameter.
QRegionRegionType is defined like this:

procedure TForm1.FormCreate(Sender: TObject);


var
Region1, Region2, Region3 : QRegionH;
Size, XStart, YStart : Integer;
APoints : TPointArray;
begin
// Region size will be the smallest dimension.
if Width > Height then
Size := Height
else
Size := Width;
// Create the points of the star.
XStart := (Width-Size) div 2;
YStart := (Height-Size) div 2;
SetLength(APoints,3);
APoints[0] :=
Point(XStart+Size div 8, YStart + Size div 4);
APoints[1] :=
Point(XStart+Size * 7 div 8, YStart + Size div 4);
APoints[2] := Point(XStart+Size div 2, YStart + Size);
// Create the first region.
Region1 := QRegion_create(@APoints[0], False);
APoints[0] :=
Point(XStart+Size div 8, YStart + Size * 3 div 4);
APoints[1] :=
Point(XStart+Size * 7 div 8, YStart + Size * 3 div 4);
APoints[2] := Point(XStart+Size div 2, YStart);
// Create the second region.
Region2 := QRegion_create(@APoints[0], False);
// Combine both regions.
Region3 := QRegion_create();
QRegion_unite(Region1,Region3,Region2);
// Apply to the form.
QWidget_setMask(Handle, Region3);
end;

Figure 6: This OnCreate method creates a six-pointed star.

first parameter is the forms handle, and the second is the handle to
the region or to a bitmap (more on this later):
procedure QWidget_setMask(handle: QWidgetH;
p1: QBitmapH); overload; cdecl;

QRegionRegionType = (QRegionRegionType_Rectangle,
QRegionRegionType_Ellipse);

procedure QWidget_setMask(handle: QWidgetH;

As demonstrated, you can create a region from a rectangle, an ellipse,


a polygon, or even a bitmap. The constructor returns a handle of type
QRegionH to this new instance. Then you can use it as a parameter
to QWidget_setMask. Either of the following members will work. The

With this brief introduction, you can convert the Windows program
shown in Figures 1 and 2 by putting the code in the forms OnCreate
event. Figure 4 shows the code to draw a round window; Figure 5
illustrates the result.

35 May 2001 Delphi Informant Magazine

p1: QRegionH); overload; cdecl;

At Your Fingertips

Figure 7: Result of
using QRegion_unite.

Figure 9: Result
of using
QRegion_subtract.

Figure 8: Result
of using
QRegion_intersect.

Figure 10: Result of


using QRegion_eor.

Using Complex Regions to Create Forms


The previous example demonstrates a simple region, but you can
combine regions to create a complex one. There are four operations
allowed on regions:
 QRegion_unite. This creates a union between two regions. The
resulting region will comprise the area covered by the two regions.
 QRegion_intersect. This creates an intersection of the two regions. The
resulting region will cover only the common parts of the two regions.
 QRegion_subtract. This operation subtracts the second region
from the first. The resulting region will have the area of the
first region that doesnt intersect with the second one.
 QRegion_eor. This creates an exclusive or of the two regions.
The resulting region will have the area from the first or second
region, but not both.
Figure 6 shows an OnCreate event that combines two triangular
regions to get a six-pointed star. Figures 7, 8, 9, and 10 illustrate the
four combination styles.

Bitmap-shaped Forms
Another way to create a non-rectangular form is using a bitmap mask.
You may have noticed the QRegion constructor at the beginning of
the article receives a QBitmapH. Dont be fooled by the name: a
QBitmapH is not the handle of a TBitmap (that would be too easy).
The handle of a TBitmap is a QPixmapH, which points to a mask
of monochromatic bits. In this format, one bit represents each pixel.
The region will be created using the bits that are turned on.
You can create a bit mask manually using a bit array, but theres
a better way: The TBitmap class has a method named Mask that
receives a transparent color and converts the bitmap into a bit mask.
36 May 2001 Delphi Informant Magazine

As an example, you can create an apple-shaped form, using the


bitmap file shown in Figure 11.
If youve run the projects, you must have noticed theres no easy way
to close the shaped forms. So, in this project, youll insert a button to
close the form, using the following OnClick event handler:
procedure TForm1.Button1Click(Sender: TObject);
begin
Close;
end;

To create the mask, you must load the bitmap, creating the mask with
the TBitmap.Mask method, and make a region from it. This is done
in the forms OnCreate event, as shown in Figure 12.
The first thing to do is to create a bitmap, and load the bitmap file.
Remember that file names are case-sensitive in Linux, so take care with
the file name used in LoadFromFile. Then, convert it to a monochrome
mask, passing the color you want to be transparent. This color wont be
drawn in the final form. Finally, create a region from this bitmap.
Although you used the Mask method to convert the bitmap to a
mask, Bitmap.Handle still is a QPixmapH and, to retrieve its mask as
a QBitmapH, use the QPixmap_mask function. This function returns
a QBitmapH with the mask of the bitmap. When you run the project,
you should get a form like that shown in Figure 13.
The function in Figure 12 is simple, but you can simplify it a bit
more. When you saw the two forms of QWidget_setMask, you probably saw you could use a region or bitmap. Using a bitmap, theres

At Your Fingertips

Figure 11: Bitmap


used to create the
form.

procedure TForm1.FormCreate(Sender: TObject);


var
BmpMask : TBitmap;
begin
BmpMask := TBitmap.Create;
BmpMask.LoadFromFile('Apple.bmp');
BmpMask.Mask(clWhite);
QWidget_setMask(Handle, QPixmap_mask(BmpMask.Handle));
BmpMask.Free;
end;

Figure 14: Simplified OnCreate event handler.


procedure TForm1.FormCreate(Sender: TObject);
var
BmpMask : TBitmap;
Region : QRegionH;
begin
// Create bitmap that will contain the mask.
BmpMask := TBitmap.Create;
// Load the bitmap file.
BmpMask.LoadFromFile('Apple.bmp');
// Convert the bitmap into a monochrome mask.
BmpMask.Mask(clWhite);
Region := QRegion_create(QPixmap_mask(BmpMask.Handle));
// Limit the drawing area to the bitmap mask.
QWidget_setMask(Handle, Region);
// Free the mask; you don't need it anymore.
BmpMask.Free;
end;

procedure TForm1.FormCreate(Sender: TObject);


var
BmpMask : TBitmap;
begin
BmpMask := TBitmap.Create;
BmpMask.LoadFromFile('Apple.bmp');
BmpMask.Mask(clWhite);
QWidget_setMask(Handle, QPixmap_mask(BmpMask.Handle));
BmpMask.Free;
FBitmap := TBitmap.Create;
FBitmap.LoadFromFile('Apple.bmp');
end;

Figure 15: Final OnCreate event handler.

Figure 12: Creating a form shaped like a bitmap.

Figure 16: Apple-shaped form with the background drawn.

This way, the bitmap is drawn in the background of the form, as


shown in Figure 16.
Figure 13: Form with the shape of an apple.

no need to create the region. The simplified version of the OnCreate


event handler is shown in Figure 14.
To enhance this form, you can draw the bitmap in the form background. To do this, the best way is to declare a variable FBitmap,
of type TBitmap, in the private section of the form, creating it
in the OnCreate event, as shown in Figure 15. You must free this
bitmap in the OnDestroy event handler using FBitmap.Free.

Conclusion
Working with the Qt API is easy and has the added advantage of
being portable between platforms (as soon as Delphi 6 ships). In
other words, if you want portability, you must use CLX and Qt API,
instead of the Windows API.
The files referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAY\
DI200105BS.

Then, you must draw the bitmap. The best place to do it is in the forms
OnPaint event handler. Use the Canvas.Draw method, as shown here:
procedure TForm1.FormPaint(Sender: TObject);
begin
Canvas.Draw(0,0,FBitmap);
end;

37 May 2001 Delphi Informant Magazine

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

New & Used


By Warren Rachele

XTNDConnect RPM
Quickly Produce Thin-client Solutions

usiness today is conducted in a wide number of places beyond the traditional


office environment. People now possess handheld computers that rival the desktop
computers of just a few years ago in power and capability. This transfer of power will
continue to be embedded in even more devices, such as cellular phones, in the coming
years. Developers easily recognize the need to extend desktop portability to the laptops of
mobile business users, but this is an increasingly unsatisfactory resolution because users
want to be unbound from the traditional computing platform. The handheld computer
has wireless connectivity options that make it an ideal platform on which to place
business applications, without the bulk of the larger laptop.
The problem is one of resources. A developer cant
expect to shoehorn a completely functional desktop
application into the 8 MB memory of a handheld.
Thin-client designs come to the forefront in this
arena, and middleware serves as a middle-tier connectivity point that can be leveraged to great advantage in
developing thin- or mobile-client applications.

In brief, XTNDConnect RPM simplifies the delivery of data. Corporate business rules and processes
are embedded at the RPM server rather than the
client, leading the way to exceptionally thin clients,
and allowing for the widest range of connectivity
and solution choices.

XTNDConnect RPM (Remote Procedure Middleware) is an embedded server that makes mobile
and Windows thin-client application development
easier. Its installed between your database or application servers, and a wide variety of client endpoints such as a Palm Pilot or desktop PC.

XTNDConnect RPM is a middle-tier server for


Palm OS and Windows applications, as shown
in Figure 1. Its extensive feature set separates it
from other middleware solutions available to developers today. XTNDConnect RPM provides consistent real-time access to enterprise servers from either
Palm OS-based mobile or Windows desktop devices.

Exploring XTNDConnect RPM

This connectivity isnt limited to database applications; any business process that lends itself to a programmatic solution can be controlled through a client
application, including report building, faxing, and
printing. Using the RPM client components with a
companys existing code base is a matter of identifying
those processes that can be removed from the client
software and migrated up to a process server.

Figure 1: XTNDConnect RPM in the enterprise.

38 May 2001 Delphi Informant Magazine

Developers will be attracted to this idea for the same


reason theyre attracted to client-server designs for
other applications; offloading an applications rules
and processes to a server puts the client software on a
diet. Lower desktop/client development and support
costs are the first advantage when client software
can be made extremely thin. Another major benefit

New & Used


derives from the existence of a single code set that is used to service the
clients. In a design familiar to many developers, remote procedures are
written once, debugged, and then shared among all of the installed base.
This is done through a connection to the database server, rather than
relying on the redistribution of constantly updated desktop applications
that contain all the regularly changing business rules.
XTNDConnect RPM extends the concept of stored procedures for
database access to other functions as well. For example, with the bulk of
the logic for the processing of a customer order stored at the server level,
a corporate development initiative can put the same capabilities on both
the desktop and the notoriously resource-deficient handheld. Imagine
trying to push all the necessary code for a report creation tool such as
Crystal Reports into your Palm OS or Windows CE machine, and you
begin to get a clearer picture of the advantages of a middle-tier solution.
In addition, the mobile user can be given complete access to corporate
data stores in real time. For example, orders can be placed immediately
into the corporate database and processed by the mobile user, rather
than resorting to the old store and forward style of updating the
database via a download-conversion-batch processing scheme.

Figure 2: XTNDConnect RPM Console.

Access to the RPM server is available through a wide variety of logical


connectivity choices. Local access is supported via cable, cradle, or
infrared connections on a handheld, and both LAN and WAN.
Remote access is supported via wired or wireless modem. This connectivity is supported via private connection or the Internet. TCP/IP
serves as the transport suite. The scalability of the product is equally
flexible, and can extend from a stand-alone desktop serving a single
user, up to a multi-user networked environment.
A middle-tier design provides a number of unique benefits over the
traditional two-tier client-server architecture. Although the database
is physically separated from the client in a client-server design, much
of the database activity is still performed by the client. This creates
a large amount of network traffic as datasets are transferred back
and forth for processing. XTNDConnect RPM moves this repetitive
activity off the client, and onto the server, improving response significantly in low bandwidth situations.
An additional, subtle benefit results from moving the database processes
off the client and onto the server. If the database structure is changed at
the server level, it will often result in the breaking of the client application. With a middleware solution abstracting the database processes up
to the server, the client is insulated from the details of the database.
The middle tier of the application provides a much more stable platform. The database server also becomes abstract to the client, so it no
longer needs any information about the server or even the database.

Using XTNDConnect RPM


As is the case with all software projects, implementing a solution using
the XTNDConnect RPM tools can be as simple or complicated as
your needs dictate. Client-server and three-tier applications require
much more time spent on the analysis and design aspects of the
project so the appropriate functionality is embedded at the correct tier.
In designing the implementation of a middleware server, consideration
must be given to what will migrate down from the application or
database server, and migrate up from the client side of the application.
Once the tiers of functionality are decided upon, project development
using the Extended Systems tools proceeds on two separate tracks.
The RPM server will be the first focus of your development process. XTNDConnect RPM remote procedures are implemented
39 May 2001 Delphi Informant Magazine

Figure 3: RPM Container in Delphis Object Repository.

as DLL server-based objects called RPM Containers. Since the


RPM Container is a DLL file, it binds to become a part of the
server itself when executed. This design lends stability and speed
of execution to the server that can be run as a Windows NT
Service, or as a Windows executable. For development purposes,
the server is run locally, allowing the developer to work alone,
then easily scale up the application to the server and a multiuser platform.
All the server processes can be controlled via the XTNDConnect
RPM Console shown in Figure 2. This server is the middle tier to
which the containers will be bound. The components included with
the Delphi SDK for building the client application also allow for
programmatic control of the XTNDConnect RPM processes.
You can examine the development process by starting off with an
examination of an RPM Container. When installed correctly into the
Delphi IDE, XTNDConnect RPM will add a new tab to Delphis
Component palette, and a new project type, RPM Container, to its
Object Repository (see Figure 3).
This container is a descendant of TDataModule, which should
be relatively familiar to most developers. A developer will add
custom remote procedures to this container object using the supplied Remote Procedure Property Editor. This interface makes
the coding needed to add these rules to your project easy. When
the rules and their data have been added to the container, the

New & Used


procedure TForm1.Button1Click(Sender: TObject);
begin
// Set up server connection properties with TrpmServer.
with rpmServer1 do begin
ServerIP := '127.0.0.1'{ Local host. };
ServerPort := 2032; { Server listening port. }
end;
// Assign container and remote procedure name
// to TrpmContainer.
with rpmContainer1 do begin
ContainerName := 'Invoice_1';
ProcedureName := 'CreateInvoice';
end;
// Build parameters required by the remote procedure.
with rpmContainer1.Params.Add do begin
paramtype := ptypeIn;
Name := 'strCustID';
Value := '155';
end;
with rpmContainer1.Params.add do begin
ParamType := ptypeOut;
Name := 'iInvoiceNum';
DataType := pdtInteger;
end;
// Execute the procedure.
rpmContainer1.ExecProc();
// Show the results.
ShowMessage(rpmContainer1.Params.ParamByName(
'iInvoiceNum').Value);
end;

XTNDConnect RPM allows developers to quickly produce thin-client


solutions for desktop and mobile users. It also simplifies the creation
of multi-tier applications and provides a central warehouse of business
rules and processes. Once inculcated by the programmer, development
moves quickly and provides an excellent return on the time invested.
Extended Systems
5777 N. Meeker Ave.
Boise, ID 83713
Phone: (800) 235-7576 ext. 5030
E-Mail: info@extendsys.com
Web Site: http://www.extendedsystems.com
Price: US$525 for one concurrent user; US$425 for five concurrent
users; see Web site for more information.

Connectivity for these client applications is available for the mobile


devices via wireless LAN or Internet, wired modem, Ethernet cradle,
or standard cradle. Support for Windows CE devices is planned.

Conclusion
Figure 4: The client code doesnt define business rules.

compilation will produce a DLL file. This DLL file is what will be
executed by the RPM server when needed by your client.
With the business rules embedded in the server, the development of
the client portion of your application is vastly simplified. The client
code wont define any business rules itself; it will merely be a shell
providing an input/output interface for the user. Notice the lack of
editing details in the example procedure shown in Figure 4.
After adding an rpmContainer and rpmServer component to the
applications form, the first few lines of code will associate them with
the RPM server. For example, the statements:
with rpmContainer1 do begin
ContainerName := 'Invoice_1';
ProcedureName := 'CreateInvoice';

identify the RPM container and the specific rule from that container
which is to be processed (a container can contain numerous rules).
The subsequent statements set up the parameters to be passed to the
remote procedure, execute it, and display the results.
This example oversimplifies the complexity of an application of any
measurable use, but it does point out the ease of use of the XTNDConnect RPM design. The learning curve is initially steep, but is
quite short. Taking the time to study the well-documented manual
provided on the disk, and understanding the service and role that
XTNDConnect RPM performs makes its use almost intuitive. All the
examples provided by Extended Systems help the learning process.

XTNDConnect RPM simplifies the creation of multi-tier applications


in much the same way that Delphi simplifies the creation of applications for Windows. The product and its use may not be immediately
intuitive to developers who are steeped in the traditions of desk-bound
development, or who may have a passing familiarity with simple clientserver designs. Middleware design and implementation requires careful
consideration of the requirements and benefits of placing a task at:
 the client,
 the database or application server, or
 in the middle, at the RPM server.
Removing the overhead of processing from the client or database
allows them to perform at their optimal pace, while creating a centralized container for business rules that also has the ability to process
them. However, once the architectural paradigm is accepted by the
developer, development moves quickly and provides an excellent
return on the time invested.
The benefits of XTNDConnect RPM to the software developer are
clear; it extends the ability to build thin-client solutions for desktop or
mobile users. Of equally great benefit is the centralized repository of
business rules and processes. No longer will the developer be required
to redistribute applications with each change in the processes. All functionality will exist in a central location that makes the process of updating them much cleaner. Consider this tool for your next development
project that will require workforce connectivity and mobility among
users, and reap the benefits of the middleware design.
Sample projects are available on the Delphi Informant Magazine Complete Works CD located in INFORM\2001\MAY\DI200105WR.

Beyond Delphi
XTNDConnect RPM supports Borlands C++Builder in the same
fashion as Delphi. If your programming activities will include development for the Palm OS, Extended Systems is ready to help you. Two
popular Palm development environments are supported: Penright!
MobileBuilder, and Metrowerks CodeWarrior. Support for the two
development tools is provided in the form of a C API or C++ classes.
40 May 2001 Delphi Informant Magazine

Warren Rachele is Chief Architect of The Hunter Group, an Evergreen, CO softwaredevelopment company specializing in database-management software. The company has served its customers since 1987. Warren also teaches programming,
hardware architecture, and database management at the college level. He can be
reached by e-mail at wrachele@earthlink.net.

New & Used


By Andrew Ghinaudo

List & Label Version 7.0


More than Just Lists and Labels from combit GmbH

ften, applications may only need to offer a user few options for lists, labels, or
reports, if there is a need at all. Many times, the options offered may only consist
of a limited set of predetermined, preformatted documents. However, some applications
must offer more comprehensive services, especially when a user expects to be able to
design custom documents to fit their specialized needs.
The requirements for listing, labeling, and reporting services may vary greatly from a simple selection on a dialog box to full design capabilities by
the user. There are numerous tools that may be
used with, or in, Delphi applications that allow
you to create these services for your users. Many of
them only allow you to create preformatted documents or documents with limited design options.

Give More to Your Users


Before I received the software package to review, I
admit that I had already formed an inaccurate idea
of what List & Label was. To me, the name implied
a software package that would produce lists and
labels. I was pleasantly surprised after I opened
the box and installed the product! List & Label
7.0 was much more than my preconception of
a few components that produced limited documents. Its an extensive tool kit that provides listing, complex reporting, electronic forms, labeling,
form letter/mail merge, and Internet capabilities.
This latest version of List & Label is packed with
various reporting capabilities. I was able to add
sub-reports that are difficult to create in other
reporting tools. List & Label offers the ability to
add scanned forms as background images with
much more precise positioning of overlaid data
fields than Im used to seeing. You also dont need
to worry about managing page breaks in contents
of fields or variables that carry over more than one
page. Of course, all the report, list, and label basics
are covered as well.

Figure 1: The List & Label Report Designer.

41 May 2001 Delphi Informant Magazine

In List & Label documents, your application


provides database independent data, or you may
choose to use data binding, which is available for
Delphi and Visual Basic. Through the provided
TDBL7_ component, you can set master and
detail data sources as needed. Whats really nice is
that you can set a few database properties, and

New & Used


fields are available for use in the visual designer, which I will explain
further later in this review.
The printing and print preview options are comprehensive. Besides
the basics, you can assign the first page and other pages to a different
printer or printer tray. This solves the annoying problem that arises
when you want the first page to print on letterhead and all following
pages to print on ordinary paper. The real-data preview allows you
to perform a final check before printing using variable zoom, and
then start the actual printing. Using the Send to option, it can grab
the whole preview and hand it to MAPI, immediately creating a new
e-mail in which the preview file (compressed if needed) is inserted.
A royalty free preview viewer can be made available to recipients so
they may view and print the attachment. An ActiveX version of the
real-data preview is also available as part of the package.

List & Label Features


There are numerous other features of List & Label version 7.0. Here
are some of the highlights:
 Definable layering and appearance options
 Hierarchical variable lists
 A table object that facilitates many reporting format options and
features
 Objects may be linked
 Formula and formatting wizards and assistants
 Classical printing, programmable data preview, or export to
HTML or RTF, are provided through a few function calls
 MAPI support
 Barcode support
 Optionally includes Unicode/Multibyte module for producing
applications for the Far Eastern market
 IntelliSense support
 Multilingual viewer application
 Delphi examples

A Distinct Difference
What really caught my eye when using List & Label was the Report
Designer, a visual designer that provides basic desktop publishing
functionality (see Figure 1). Whether its you, the developer, creating
reports, lists, or labels for your end users, or you making the designer
available in your applications for your users to access, the Report
Designer provides a comprehensive GUI that speeds the process. All
your data and variables are visible from a tool window that allows
you to simply drag and drop them onto the document. Placement of
objects in the designer is quick and precise.
The variables list that I mentioned is represented in a well-organized
TreeView. You can extend the list and add more variables and expressions as needed. Assistance is provided in a dialog box that allows you
(or your user) to utilize predefined expressions and functions, or you
can build your own. Many of the predefined Avery label templates
are provided as well.

The Bottom Line


List & Label is available in DLL, ActiveX, and VCL forms and is
compatible with Delphi, C++Builder, Visual Basic, Visual C++ and
a slew of other development platforms. The only downside is that at
the heart of it all are some DLLs, so you will have to ship more than
just your EXE and application-related files. But this isnt much of an
obstacle when compared to some other reporting tools.
Very important to me are the issues of royalties and support.
Personally, Id rather pay a larger one-time upfront cost for a
42 May 2001 Delphi Informant Magazine

I liked what I saw in List & Label, namely the Report Designer where
I could create predefined reports, lists, and labels right in the Delphi
IDE, and at design time. Even more appealing is being able to ship a
visual designer as part of my application so users may create their own
documents. However, it may be a little difficult for some end users.
combit GmbH
Untere Laube 30
78462 Konstanz
Germany
Phone: (49) 7531 906010
Fax: (49) 7531 906018
E-Mail: info@combit.net
Web Site: http://www.combit.net
Price: List & Label 7.0 for
Windows 95/NT 4, US$501.07;
Delphi-only, US$357.90.

Distributed in the
United States by:
BeCubed Software, Inc.
170 South Church Street
Canton, GA 30114
Phone: (888) becubed
Fax: (770) 720-1078
E-Mail: sales@becubed.com
Web Site: http://
www.becubed.com

tool kit than to require my users to pay ongoing royalties. Fortunately, there are absolutely no royalties for List & Label, including
the Report Designer. Plus, the cost is very reasonable. You can
purchase the Delphi-only version of List & Label for US$357.90
(at the time of this review) from the combit Web site at http://
www.combit.net.
Support is free for the first thirty days, starting at the time of your
initial support request. Afterward, combit charges for support with
a Point Card system (see their Web site for details). This includes
written support requests such as fax, BBS, or e-mail, as well as
telephone support.

Conclusion
I liked what I saw in List & Label, namely the Report Designer
where I could create predefined reports, lists, and labels right
in the Delphi IDE, and at design time. Even more appealing is
being able to ship a visual designer as part of my application so
users may create their own documents. The built-in wizards and
assistants were also helpful.
However, there are a few tradeoffs to consider when incorporating
List & Label. Along with the complex document creation and other
capabilities List & Label has to offer, it may be a little difficult for
some users to grasp (if you ship it with your applications), even with
the Report Designer. This in turn may require you to offer some
additional training for purchasers of your applications who may not
be computer savvy. If you are in the market for such a tool, I suggest
you try the demo of List & Label. It might just be what youre
looking for.

Andrew Ghinaudo is Manager of Enhanced Services Development at Premiere


Conferencing located in Colorado Springs, CO. He also maintains his own small
development/consulting business, Media Micronics. He is currently the Assistant
Technical Editor for Delphi Informant Magazine. For information concerning Media
Micronics consulting services send e-mail to info@mediamicronics.com.

Best Practices
Directions / Commentary

As Simple as Possible

hat does ASAP stand for? You may be familiar with the phrase as soon as possible as an answer to, When
does the software project need to be finished? I find that a more important phrase for programmers to learn
is as simple as possible. If software is kept as simple as possible, it will among other benefits increase your
chances of getting it done as soon as possible.
Simplicity is important, both in the user interface and the code
beneath it. Keeping things simple isnt the sign of a simple mind.
Its easy to make something difficult to understand; it requires much
more intelligence to make something that is difficult seem easy, or
at least approachable.
Due to time constraints or seemingly unique requirements, we may
feel justified at times using out-of-the-ordinary UI designs. Be careful!
Users are accustomed to certain ways of doing things Windows
usage patterns, if you will. Windows programs have a certain look and
feel, and modus operandi, that we would do well to adhere to as much
as possible. If we confuse the user, we make things more difficult
for ourselves in the long run. And not only for ourselves also for
trainers, and those who support the end users.
The foundation behind the facade is the code, which the end users
will probably never see. Your audience is admittedly a more technical
one, but even here its beneficial to keep things ASAP. As youre
coding, you have the high level, overall view firmly in your mind, and
are also concentrating on the low-level intricacies of the implementation. The code seems readable enough now. But what about in a
year, or even a month, or a week when you look at it again?
Im sure most of you have had the same experience when returning
to maintain previously written code confusion and consternation.
What does this method do? And why? Why was it implemented this
way? Why wasnt it done this way instead? You can save yourselves
and others a lot of grief, agony, frustration, and time by keeping your
code ASAP. Albert Einstein probably said it best when he advised,
Things should be made as simple as possible, but not any simpler.
Of course, ASAP means as simple as possible while producing software that your sponsors, employer, client, or users consider successful.
The traditional Hello, World program is simple (see http://
www.ariel.com.au/jokes/The_Evolution_of_a_Programmer.html for a
poor example of ASAP), but it probably wouldnt meet the needs of
even your least demanding customer or employer.
A potential roadblock to keeping your programs ASAP is the tendency of management to allow or demand feature creep to
muddy the waters. They often think adding more bells and whistles
will equate to more revenue. We coders are also often our own worst
enemies as a result of our tendency to gold plate adding cool,
but unnecessary features. If we were to be honest with ourselves, our
prime motivation in adding some features is our fascination with the

43 May 2001 Delphi Informant Magazine

how of providing the feature, rather than a conviction of its true value
to the user. What we think is cool may be superfluous to the user.
By now youre hopefully convinced of the necessity, or at least
the desirability, of keeping your code ASAP. So how can you go
about doing that? Three simple ways: Avoid inconsistent or extreme
formatting/indentation styles, steer clear of cryptic or nonsensical
method and variable names, and shun very long methods.
As far as formatting style goes, a good pattern is provided by Borlands own source code. You will notice the balance they use, for
example, in the number of spaces they indent it is neither miniscule (one space, as in some code Ive maintained), nor does it go to
the other extreme of six spaces.
As to method and variable names, I have seen a variable named
IsValid. A Boolean, right? In fact, it was a global string variable! Im
sure you have seen function names that sounded like procedures, and
vice versa. Make the purpose of your variable and method names as
obvious as possible by using clear and accurate names.
Another way programmers commonly make matters overly complex and
confusing is by writing long blocks of event handler code. It is preferable
to make calls to procedures and functions (perhaps in another unit) in
these cases, making the event handler code ASAP, so the maintainer can
see at a glance whats supposed to happen in response to that event.
Each method should be atomic; it should do one thing and one thing
only. If several related operations need to be performed, break them into
separate methods, and have them call each others services as needed.
The bottom line: If you want to win friends and influence people
with your software, keep it ASAP.
Clay Shannon

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


operating as Clay Shannon Custom Software. He is available for remote development, short-term on-site assignments, as well as select long-term assignments
in the Spokane, WA/Coeur d Alene, ID area. To view his resume, go to
http://www.sysadminsrus.net/clayshannon/ClayShannon.doc. You can reach him
at bclayshannon@earthlink.net.

File | New
Directions / Commentary

Quality First, Revisited: Part II

ast month we returned to the topic of Quality First (first addressed by me in this column in the July 1998 issue) by
examining three excellent books that discuss various aspects of ensuring quality in our development projects. We also
took a look at the concept of quality in the beginning, or how to lay a proper foundation for such a project.
This month well continue to explore further insights from Code
Complete [Microsoft Press, 1993] and After the Gold Rush [Microsoft
Press, 1999] by Steve McConnell and Constructing Superior Software
[Macmillan Technical Publishing, 1999, Paul C. Clements, ed.]. Lets
turn our attention to what happens in the middle stage of creating
new software.
Quality in the middle. The middle part of a software development
project generally revolves around coding. Its worth repeating some
of Bertrand Meyers insights from my July 1998 column. Recall that
Meyer recommends you build [an application] so you can trust it.
Then dont trust it. He further recommends beginning with the
cosmetics, by which he means using correct syntax, writing readable
and well-commented code, and sticking to a consistent coding style.
More importantly, he recommends we get everything right from the
start and fix it immediately if it is not.
A complex project might also include some design tasks. Of course,
in Barry Bohems spiral process described in Chapter 3 of Constructing Superior Software, the seemingly preliminary task of planning, analysis, and design continue throughout a project, becoming
more refined when informed by results of implementation tasks.
McConnell points out in After the Gold Rush that ideally, although
planning and process-management continue throughout a project,
emphasis on this should be high at the start, then decrease as the
project continues.
Emphasis on quality should be just as high during the implementation/
coding phase as in the initial planning phase. This means that one
should not only use best practices in coding, but also be willing to go
back and modify the architecture or design, if a flaw becomes apparent. This is much less costly in wasted time if its done immediately.
During the middle phase, we should also introduce a task normally
associated with the end of a project: testing and debugging. Following
Meyers precept previously quoted, our goal should be to remove
most defects before we release any software. In After the Gold Rush,
McConnell states that projects that remove about 95% of their
defects prior to release are the most productive; they spend the least
time fixing their own defects.
44 May 2001 Delphi Informant Magazine

One of the most important tasks during the middle phase of a


large project is managing the team or teams. Chapter 7 of Constructing Superior Software includes a good deal of helpful information
and advice regarding teamwork considerations for creating superior
software development, and for defining the roles of team members
to avoid the chaos trap. If you are the sole or principal coder
for a project, you must likely assume multiple responsibilities, such
as defining the problem to be solved, working on design, and,
of course, writing code. You may have to take time to learn new
technologies, techniques, or tools; youll certainly need to be mindful of managing your resources.
Related to these multiple challenges, McConnell explains the
diverse knowledge areas with which the ideal software engineer
should be acquainted. These include the obvious disciplines of
computer science and mathematics, as well as quality engineering,
other engineering disciplines, cognitive sciences, and project management. These provide the basis for performing a plethora of
software-development tasks, from software construction itself to
testing, from design to maintenance, and from requirements analysis to project management. Obviously, not every software engineer
will be highly proficient in every area. But an engineer who has
some familiarity with all of these subject areas is going to be able to
make a more significant contribution to quality at every stage in a
projects development including the middle stage.
Quality in the end. Quality in the end means shipping no product
before it is ready. Needless to say, quality at the final stage is dependent
upon quality in the beginning and quality in the middle. Quality
assurance testing becomes the major focus at this stage, but it should
be an element of the development process from the outset. We discussed architecture last month. A key element of architecture is how
the product will be used. Even at this early stage you should consider
what tools and approaches would be appropriate for testing the usefulness of the product being developed. If you use an evolutionary prototyping approach, its easy to begin testing early in the development
process. You can get early feedback on the user-interface, discover basic
flaws when they are easiest to fix, and, best of all, establish a testing
architecture that can be used until the product ships.

File | New
Another task that is worth considering at the end is a thorough analysis
of the entire project. Such introspection can pay great dividends for
future development of the current project or the company as a
whole. Developers and testers can identify shortcomings that will be
addressed by new features in future versions, make certain that support
personnel have sufficient information to address customer needs, and
make absolutely certain that the product is ready to ship. An endof-project assessment also provides an opportunity for a company to
improve its overall effectiveness, moving through the levels of the Software Engineering Institute Capability Maturity Model for Software that
McConnell discusses in After the Gold Rush. Such analysis would be
largely absent at the first two levels: initial and repeatable. At this second
level, the emphasis is about getting beyond the code-and-fix approach.
Such an assessment might be introduced by an organization thats at
the defined phase, characterized by standardized technical and management processes. It would certainly be present in the most mature
organizations, those that are managed ... to evaluate the effectiveness
of different processes and optimizing organizations that emphasize
improving the processes themselves by engaging in constant assessment
at all levels. Clearly, an emphasis on quality throughout a project can
impact not just the work at hand, but future endeavors as well.
Conclusion. We can help ensure quality in the beginning by not
jumping too quickly into coding, thus avoiding the code-and-fix trap.
Newer tools such as design patterns and UML can be helpful (this will
be the topic of an upcoming column). We can ensure quality in the
middle by using best practices in our coding. There is hardly a better
single source of best practices than McConnells Code Complete. Of
course, Delphis component-based, object-oriented structure gives us a
mighty advantage. We should also consider an evolutionary prototyping approach in which we incorporate testing and debugging early in
a projects development. We can ensure quality at the end by making
certain that the product to be released has been tested by a variety of
users in a variety of environments. And before we finish, we should
take a long look back to assess all of the procedures we have used from
the start, so we can work more efficiently next time.
I hope that you have found these ideas helpful in your work; but
please bear in mind that most of them are borrowed ideas on which
I have elaborated. I have relied heavily on the three excellent books
described throughout this two-part series; I highly recommend each
one. Until next time....
Alan C. Moore, Ph.D.

Alan Moore is a Professor of Music at Kentucky State University, specializing in


music composition and music theory. 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.

45 May 2001 Delphi Informant Magazine

Vous aimerez peut-être aussi