0% found this document useful (0 votes)
49 views

Delphi Informant 95 2001

This document summarizes new products and solutions for Delphi developers that were announced in March 1999. It describes new releases of AutoSQL from OCERIS that generates SQL code from existing database tables. It also describes new versions of SQLQuery from Component Store for building thin database clients, ClassExplorer Pro from toolsfactory for object-oriented code navigation and documentation, and LMD-Tools from LMD Innovative with over 150 Delphi components. Finally, it describes Raize Components II from Raize Software with over 30 new controls.

Uploaded by

reader-647470
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
0% found this document useful (0 votes)
49 views

Delphi Informant 95 2001

This document summarizes new products and solutions for Delphi developers that were announced in March 1999. It describes new releases of AutoSQL from OCERIS that generates SQL code from existing database tables. It also describes new versions of SQLQuery from Component Store for building thin database clients, ClassExplorer Pro from toolsfactory for object-oriented code navigation and documentation, and LMD-Tools from LMD Innovative with over 150 Delphi components. Finally, it describes Raize Components II from Raize Software with over 30 new controls.

Uploaded by

reader-647470
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
You are on page 1/ 37

March 1999, Volume 5, Number 3

Cover Art By: Darryl Dennis

ON THE COVER 25 Undocumented


6 Active Server Pages — Ron Loewy Shell Notifications — Kevin J. Bluck and James Holderness
At the foundation of Microsoft’s Web-development architecture, ASP has It’s undocumented, so just how does Windows keep itself abreast of cur-
become popular as well. Mr Loewy explores the ASP object model, and rent events, such as a file being created or moved? Misters Bluck and
discusses how to take advantage of it from a Delphi-developed Holderness reveal all the secrets, including something called PIDLs.
Automation object — an object available for download.

FEATURES REVIEWS
12 Patterns in Practice 34 ReportBuilder Pro 4.0
The Singleton Pattern — Xavier Pacheco Product Review by Bill Todd
Mr Pacheco begins a new column devoted to design patterns, and their
creation and use with Delphi. He gets things started with the Singleton
pattern, which ensures only one instance of a class exists.
DEPARTMENTS
21 Multi-Tier 2 Delphi Tools
The Briefcase Model — Bill Todd 5 Newsline
The Briefcase Model is easy to understand: download part of a database 37 File | New by Alan C. Moore, Ph.D.
onto a laptop, and run. Now, implementing it — with or without MIDAS
— is also relatively straightforward, as Mr Todd explains.

1 March 1999 Delphi Informant


OCERIS Ships AutoSQL 2.1
Delphi OCERIS, Inc. announced ond and third out-
T O O L S AutoSQL 2.1, which takes puts are Delphi code
existing Paradox, dBASE, compatible with ver-
New Products and Access 95/97 tables and sions 2, 3, and 4,
and Solutions creates three types of out- and C++Builder
put. The first is CREATE code compatible
TABLE and CREATE with C++Builder 3.
INDEX statements for cre-
ating the tables in a SQL OCERIS, Inc.
environment. It will option- Price: US$49.95
ally create INSERT INTO E-Mail: oceris@oceris.com
statements for the data Web Site: http://www.
inside the tables. The sec- oceris.com

Component Store Announces SQLQuery 2.2


Component Store Ltd. for use with Delphi. Server as a back end. New
announced the release of SQLQuery 2.2 can be used to features in version 2.2 include
SQLQuery 2.2, an add-on build thin Win32 or ActiveX direct update without
database component designed clients with Microsoft’s SQL TSQLUpdate through the
UpdateParams property, sup-
toolsfactory Announces ClassExplorer Pro 2.1 port for optimistic concurren-
toolsfactory GmbH (LLC) plex projects. Class member cy in UpdateParams, asyn-
announced ClassExplorer Pro creation features simplify the chronous Open and
2.1, an integrated software creation of methods, proper- ExecSQL, InfoPower support,
development tool that pro- ties, and fields to classes. Code local sort of fields in a result
vides object-oriented code documentation features offer set, and added dataset events
navigation, creation, and doc- customizable, automatic, for easier SQL error handling.
umentation for Inprise’s online-help generation from
Delphi and C++Builder devel- source code, including indexes Component Store Ltd.
opment environments. and hierarchical tables. Price: US$249 for a single-developer
ClassExplorer Pro 2.1 sup- license; multiple developer and site
ports features such as Class toolsfactory GmbH (LLC) licenses are available.
View and Class Hierarchy, Price: US$99 Phone: (800) 903-4152
which simplify the source view E-Mail: sales@toolsfactory.com Web Site: http://www.
and class navigation of com- Web Site: http://www.toolsfactory.com component-store.com

LMD Innovative Releases LMD-Tools 4


LMD Innovative tion projects, and
released the LMD- WPTools Light
Tools 4 component (LMD-Edition),
package, a set of RTF-Editor, and
native Delphi VCL RTF-Label.
components and Version 3.5 is
routines for various also included for
programming tasks. compatibility
Version 4 includes with Delphi 1
customizable, trans- and 2 and
parent edit and C++Builder 1.
memo controls (without data resources (now sup-
text limitations); an exten- porting native data com- LMD Innovative
sive FileGrep component; pression). Price: Standard Edition, US$149;
Calendar controls; an Both the Standard and Professional Edition, US$199 (includes
enhanced dynamic-splitter Professional editions source code of the component library,
component; dockable tool- include over 150 compo- additional add-ons, and C++Builder
bars; and improved han- nents for Delphi 3 and 4 support).
dling of data containers for and C++Builder 3, online Phone: +49 271 355489
bitmap-/wave-files or other help, over 60 demonstra- Web Site: http://www.lmd.de

2 March 1999 Delphi Informant


Delphi Raize Announces Raize Components II
Raize Software Solutions, Components can appear as Delphi 4 developers. For
T O O L S Inc. announced Raize line-style controls. In addition, example, the TRzPanel,
Components II, the next gen- the custom framing properties TRzSizePanel, and TRzSplitter
New Products eration of the company’s support showing a second components utilize a custom
and Solutions library of native VCL controls frame style whenever the docking manager to manage
for Delphi and C++Builder. mouse is positioned over the controls docked in their client
Raize Components II intro- control, or the control receives areas. The new docking man-
duces over 30 new compo- the input focus. ager displays the captions of
nents, including The majority of components docked controls instead of the
TRzCheckTree, in Raize Components II have default “grabber” bars.
TRzBackground, TRzButton, an associated context menu Raize Components II pro-
and TRzEditListBox. that provides quick access to vides support for Delphi ver-
TRzCheckTree is a tree view common settings and proper- sions 1, 3, and 4, and
control that associates a check- ties without requiring the C++Builder 3.
box with each node in the developer to switch to the
tree, and automatically updates Object Inspector and search Raize Software Solutions, Inc.
the states of parent and child for properties. Price: US$249
nodes when the state of the Raize Components II intro- Phone: (630) 717-7217
current node changes. The duces specific features for Web Site: http://www.raize.com
TRzBackground component
enables developers to add gra-
dients and tiled textures to
forms, including MDI frames.
The TRzButton component
supports multi-line captions,
3D text styles, and custom
button face colors. The
TRzEditListBox supports auto-
matic run-time editing of
items in the list using a popup
edit window.
Raize Components II also
introduces custom framing
properties, which allow devel-
opers to select which sides of
the frame will appear. As a
result, all the Raize
Primoz Gabrijelcic Announces GpProfile 1.1
Primoz Gabrijelcic threaded program support, the trol. With GpProfile 1.1, pro-
announced the availability of ability to instrument proce- filing results can be exported to
GpProfile 1.1, a profiler for dures (written in built-in standard delimited format.
Delphi 2, 3, and 4. assembler), the ability to In addition, GpProfile 1.1
GpProfile 1.1 is a source- show/hide an integrated offers conditional API execu-
instrumenting profiler that Source Preview window, a syn- tion with metacomments, a
works with Windows 95/98 tax-highlighted source preview, layout manager, the ability to
and NT 4/5. It features multi- and an API for profiling con- display and browse
caller/called hierarchy, context-
CenturionSoft Announces EuroFonter sensitive help, and free, com-
CenturionSoft announced EuroFonter offers a wizard- plete source code (Delphi 4).
EuroFonter, a utility that style interface that makes it
adds the euro symbol to all easy to install and use. Primoz Gabrijelcic
TrueType fonts. Any docu- Price: Free
ment created and/or received CenturionSoft E-Mail: primoz.gabrijelcic@
will display the correct sym- Price: US$39.95 altavista.net
bol, avoiding the risk of dan- Phone: (202) 293-5151 Web Site: http://www.eccentrica.
gerous misunderstandings. Web Site: http://www.centurionsoft.com org/gabr

3 March 1999 Delphi Informant


Delphi Datasoft Reveals GhostFill SDK
Datasoft (Pty) Ltd integrate with GhostFill and functional copy of GhostFill.
T O O L S announced the release of the produce documents from GhostFill’s COM-based
GhostFill Software Developers within their applications — architecture encourages devel-
New Products Kit (SDK), a developers kit even across the Internet. opers to extend its function-
and Solutions for the company’s productivi- The GhostFill SDK includes ality by adding custom OLE
ty add-in for Microsoft an ActiveX control for inte- automation servers to its
Word. gration with Delphi, environment.
GhostFill is a document Microsoft Visual Basic and
assembly tool that simplifies Visual C++, and others. It Datasoft (Pty) Ltd
and expedites the production also ships with a set of sample Price: Free for download.
of complex documents. With applications, comprehensive Phone: +27 21 683 4680
the SDK, developers can documentation, and a fully Web Site: http://www.ghostfill.com/sdk
Eagle Software Releases CDK 4 and reAct 4
Eagle Software announced after generation is unnecessary. trol panel lights flash when
CDK 4, a suite of code gen- CDK 4 employs a code corresponding events are
eration and modification reuse engine, allowing users to triggered.
wizards that integrate with drag and drop “smart code” reAct 4 also includes
Delphi 4. The company (CDK Templates) into their built-in streaming tests,
also announced reAct 4, a class designs. CDK 4 ships making it easier to verify
test program generator for with 34 CDK Templates and the component’s ability to
Delphi. a wizard for creating custom save and load its state infor-
CDK 4 includes wizards CDK Templates. mation to a file.
for building descending, Also announced was reAct Because test programs
composite, business, link- 4, which generates the code generated by reAct are
ing, embedded, and dialog needed to evaluate any Delphi programs, users are
components, as well as selected component. reAct free to modify and enhance
property editors, compo- 4 test programs consist of a the generated source as
nent editors, and packages. run-time component needed. reAct 4 is fully
The CDK Package Wizard inspector, a test form, and compatible with other
allows developers to build an event log. third-party testing tools
package sets consisting of a With reAct 4, users can that work with Delphi.
design-time package and one dynamically create and
or more run-time packages. destroy instances of the test Eagle Software
CDK separates and main- component, view and Price: CDK 4, US$289 per copy; reAct
tains run-time and design- change properties, and see 4, US$139 per copy; site licenses are
time code, so users are free the effects of those changes available for both products.
to focus on the essence of on the test component at Phone: (310) 441-4096
class design. run time. Users can see Web Site: http://www.
CDK 4 can also modify any events as they occur; con- eagle-software.com
existing Delphi
source code. CDK’s
modification engine
parses, then folds,
new code directly
into the source file.
CDK 4 allows users
to edit generated
source code manually
(in Delphi) and con-
tinue to modify the
source with CDK
wizards interchange-
ably. Also, CDK gen-
erates all code in the
user’s coding style, so
reformatting by hand

4 March 1999 Delphi Informant


News Inprise Launches CORBA and Java Tour for Enterprise IT Managers
Scotts Valley, CA —
Inprise Corp. announced
intranet and Internet appli-
cations, whether they are
standardized reusable n-tier
components.
L I N E a North American series written in Java, C++, or For more information on
of management-level other languages. Distributed Enterprise
March 1999 CORBA and Java seminars “Building Distributed Solutions on Tour, and to
designed to show IT execu- Applications with Java” will register for a seminar, visit
tives the strategic business include discussions on the http://www.inprise.com/
benefits of large-scale, design and development of events/seminars, or call
platform-independent, and distributed systems, the (800) 255-4388. The standard
standards-based enterprise deployment and manage- registration fee is US$395 per
application development. ment of Java applications, person for one day, and
Distributed Enterprise the benefits and drawbacks US$665 for both days. Special
Solutions on Tour consists of two- and three-tier com- corporate/group discounts
of two one-day seminars: puting, and the use of are available.
“CORBA Essentials for Enterprise JavaBeans as
Effective Internet
Computing” and “Building Inprise Delivers Enterprise Application
Distributed Applications Server Solution
with Java.” Participants can New York, NY — Inprise an enterprise’s competitive-
attend one or both days of Corp. introduced the ness by streamlining its IT
the series. Inprise Application Server, processes and application
“CORBA Essentials for a solution that accelerates lifecycle.
Effective Internet and simplifies the develop- Key features of the Inprise
Computing” will cover the ment, integration, deploy- Application Server include
role CORBA plays in enter- ment, and management of support for all major pro-
prise systems and how to distributed enterprise appli- gramming languages, client
LEAD Announces VCL use distributed objects to cations. The Inprise interfaces, Web servers,
Components for Delphi
LEAD Technologies, Inc. bring high performance to Application Server enhances database servers, hardware
announced support for Inprise platforms, and legacy envi-
Corp.’s VCL (visual component Inprise Announces New Version of MIDAS ronments, including DCE
libraries). The new LEADTOOLS
VCLs will offer developers the Scotts Valley, CA — Inprise object/relational databases. and COM; an object-com-
same imaging functionality as the Corp. announced version 2 of Finally, through integration munications infrastructure
LEADTOOLS ActiveX controls, built with Inprise
but will provide this functionality
Inprise’s MIDAS (Multi-tier with the Inprise Application
in a native Delphi component Distributed Application Server, customers can man- VisiBroker; an open and
format. The VCL will be available Services), which simplifies age their MIDAS-produced extensible architecture
throughout the LEADTOOLS based on industry stan-
product line.
and hastens the development, applications.
integration, and deployment One of the key offerings dards, such as CORBA,
of thin-client, distributed- of MIDAS 2 is MIDAS C++, Java, and HTML;
database applications. Client for Java, which sim- support for the Sun Solaris,
MIDAS speeds data access plifies the development of HP-UX, IBM AIX, and
across all application tiers, cross-platform, Pure Java Microsoft Windows NT
from the client to the data- thin clients for distributed- platforms; JBuilder for
base server, through remote- database applications. Application Server; Web
data access and intelligent MIDAS Client for Java deployment of Internet-
data synchronization. includes a set of Java Beans, based, platform-indepen-
Enterprises can now devel- or components, designed dent applications;
op MIDAS applications for JBuilder 2. These Pure VisiBroker Integrated
with all of Inprise’s enter- Java components give devel- Transaction Service (ITS);
prise tools, including opers cross-platform client and AppCenter, a distrib-
JBuilder, Delphi, and access to high-performance uted applications-level
C++Builder. In addition, multi-tier MIDAS applica- management tool.
MIDAS 2 supports Java, tions. For more information and
CORBA, and COM/MTS. For more information and pricing, call Inprise direct
MIDAS 2 now includes pricing, call Inprise direct corporate sales at (831)
support for intelligent mas- corporate sales at (831) 431-1064, or visit
ter/detail and nested tables, 431-1064, or visit http://www.inprise.com/
as well as Oracle8i http://www.inprise.com/midas. appserver.

5 March 1999 Delphi Informant


On the Cover
ASP / HTML / Automation / Delphi 3, 4

By Ron Loewy

Active Server Pages


Building ASP Controls with Delphi

A ctive Server Pages (ASP) is at the foundation of Microsoft’s Web develop-


ment architecture. ASP is an extension to Microsoft’s IIS (Internet
Information Server) Web server, and is available free of charge on any operat-
ing system that has IIS or Microsoft’s PWS (Personal Web Server) installed. This
means most new machines with Windows 98 or Windows NT come with ASP.
Adding ASP to older Windows 95/NT installations is easy; the code is available
for download from Microsoft’s Web site. It’s also shipped with several other
Microsoft tools, such as Visual InterDev, FrontPage, etc.

ASP is implemented as an ISAPI extension to Extending the functionality of ASP-writing


IIS. Think of it as an optimized CGI pro- Automation objects is easy. The ADO article I
gram for Microsoft’s Web servers that can just mentioned shows how to create a simple
serve HTML pages created dynamically using Automation object that can access a database
some logic. These are not your run-of-the- using ADO, and can be used from an ASP
mill, static HTML pages; code is used to application. The Automation object developed
process input from the user, access databases, in that article did not know it was used from
or use some other mechanism to create what an ASP application, and could be used from
users see in their Web browsers. any COM-enabled development tool. In this
article, we’ll explore the ASP object model,
Writing ASP applications is relatively easy. and discuss how to take advantage of it from a
ASP code is a combination of HTML and Delphi-developed Automation object. We’ll
scripts written in JavaScript, VBScript, or also create the Automation object (the project
any other ActiveScript-capable language described in this article is available for down-
engine. Microsoft provides a set of pre- load; see end of article for details).
defined Automation objects that can be
used from ASP applications. With some of The ASP Object Model
the common ActiveX and Automation Most of Microsoft’s new APIs appear as a col-
objects available on Windows, writing data- lection of objects that represent the task at
base-driven applications for the Internet hand. In Microsoft speak, this is commonly
and/or an intranet is easy. (See my article referred to as an object model. Some of the
“Much ADO about the Web” in the examples for the use of object models as APIs
December, 1998 Delphi Informant.) You can include the Document Object Model (DOM),
write ASP applications using your trusty which allows object-oriented access to the con-
copy of Notepad (or EDLIN, if you’re a real tents of an HTML document; the ActiveX
masochist), but most people like to use one Data Objects (ADO), which encapsulate the
of the tools that support ASP development. database access primitives as a set of objects;
Microsoft’s Visual InterDev comes to mind, and true to form, the ASP object model, which
and there are many other third-party solu- represents the transactions and entities that cre-
tions to choose from. ate a Web application as a set of objects.

6 March 1999 Delphi Informant


On the Cover
There are five objects you’ll need to learn to write efficient The ServerVariables collection provides information about
Web applications with ASP: pre-defined environment variables that were passed by the
1) The Request object is used to hold information about the Web server. For example:
request the user sent when accessing the application.
2) The Response object is used to encapsulate the HTML Request.ServerVariables("PATH_TRANSLATED")
response the application will send back to the browser.
3) The Server object is used to manage the ASP environ- will return the path of mypage.asp on the local machine’s file
ment. You will usually use it as a launching pad for your system. This can be very useful if you need to access external
own ActiveX and Automation objects. media, or other files that are installed in the same directory as
4) The Session object is used to store information specific to your ASP pages.
the user of the application. This allows the application to
maintain state for a specific user (which requires using The Form collection provides information to values passed
expanded URLs, or cookies in raw CGI programs). in form controls. If, for example, our user entered a credit-
5) The Application object is used to store and manage card number in a form defined using the following HTML
application information shared among all sessions of code snippet:
the application.
<Form Action="http:// path-to-server/mypage.asp">
The Request Object ...
Credit Card Number <input name="CreditCard" Size=16>
The Request object encapsulates the information sent from ...
the client’s browser to the Web server when the user requests
a resource from the ASP application. To understand the we can access it using the following syntax (in JavaScript):
Request object, let’s look at what happens when a user clicks
on a link to a page in the ASP application: CodeToCheck = Request.Form("CreditCard");
The user clicks on the link’s title.
The browser uses the anchor information defined in the Finally, cookies can be accessed via the Cookies collection:
<A...> tag that defines the link to find the server that
hosts the ASP application. After a connection is estab- Last time you visited was <%= Request.Cookies("LastVisit") %>
lished, an HTTP GET request is sent to the server with
the path to the ASP page. You can do more with the Request object and the different
If the URL had additional information to pass to the appli- properties and collections it provides. Any good ASP refer-
cation using the HTML URL?Parameters syntax, the para- ence will include all the information.
meters are sent to the application as part of the request.
Additional information about the user and the browser is The Response Object
sent in HTTP headers along with the request. The Response object allows you to create the output the user
If the user’s browser has had cookies defined in the past will see in the browser as a result of his or her request. It pro-
for the resource, these cookies are sent in additional vides access to the HTTP headers that are sent with the
HTTP headers. request, and allows you to build the response from start to end.

If the user fills out a form and clicks the Submit button that Because the result is built sequentially from the headers to
connects to the ASP application via a <FORM ...> tag (whose the end of the response, you have to set the values of the
action points to the ASP application), the request sequence is headers, cookies, etc. before you write the content of the
repeated. Usually, however, an HTTP POST request is sent, result. The Response object has a property called Buffer that,
and the names of the variables in the forms and the values the when set to True, caches all the output, and doesn’t send it
user entered are sent after the HTTP headers. to the user until the Flush or End method has been called. If
you don’t want to write sequentially to the output, remem-
When the request is received by the Web server, it determines ber to set Buffer to True at the start of your ASP page.
this is a request for an ASP script, using the main program’s
extension (e.g. mypage.asp), and the ASP interpreter is acti- The following properties can be used to set the HTTP
vated with the request information. headers of the page:
The Cookies collection can set cookies that are related to
ASP parses the request information and allows you to access it as your ASP page. You will get these cookies in the Request
properties and collections of the Request object. The QueryString object the next time the user connects to your page.
property, for example, includes all the parameters passed in the The ContentType property sets the type of response you
URL after the ? character. If, for example, the URL was send. By default, text/html is assumed, but if your
http://path-to-server/mypage.asp?Name=Ron, we could find the response is an image, you’ll need to set it to image/gif,
value of the Name parameter using the following syntax: image/jpeg, etc.
The Status property sets the status that is returned. By
Your Name is <%= Request.QueryString("Name") %> default, “200 OK” is returned, and the browser will dis-

7 March 1999 Delphi Informant


On the Cover
play the response. If, however, the user did not provide will get confused and think that < 2 ... is a tag it doesn’t
information, you can set a different response (e.g. “401 recognize, and will ignore your code.
Unauthorized” if the user’s password wasn’t found in your
database). The most important function of the Server object is the
The Expires (or ExpiresAbsolute) property can be used to CreateObject function. This function is used to start an
define when the response expires from the browser’s Automation object. As a Delphi programmer, the CreateObject
cache, and the browser needs to connect to the server function is the way you can start your Delphi-developed
again to receive an updated page. Assume, for example, objects to interact with an ASP application. The CreateObject
that you write a stock ticker application that displays up- function takes a ProgID as its parameter. You can determine
to-date stock prices. Assuming your database is updated the ProgID of your Delphi object from the Type Library edi-
every 15 minutes, set Expires to 15 to ensure that if the tor. It’s the name of the library separated by a dot from the
user tries to access the page in 10 minutes, no network name of the Automation object, e.g. MyLib.MyObj.
bandwidth will be used. But if the user tries after 20
minutes, he or she will get updated information. The Session Object
HTTP is a stateless protocol. When a client connects a Web
You can also write any HTTP header using the Response server, the Web server answers and disconnects. The next
object’s AddHeader method. Another method related to time you connect to the server, the server has no indication
headers is the Redirect method. Use this method to redirect that you called it earlier, and has no way to tell your connec-
the browser to a new URL. This can be useful if you need tion from a connection made by another user hundreds or
to route users to different URLs based on their cookie val- thousands of miles away from you.
ues, or other information they provide.
Imagine a simple shopping application. The user wants to
After the headers have been set, it’s time to create the content. browse the available products and collect them in a virtual
ASP creates the content from the HTML code in the page; shopping basket. When finished browsing, the user
your scripts or Automation objects can write to the output advances to the checkout line and pays for the selected
stream using the Response object’s Write method. For example: items. If your application doesn’t save the state of the user’s
shopping basket, how will you know what items the user
<% wants to purchase? How will you be able to differentiate
if (SomeCondition) { between user A, who wanted to purchase the US$300
Response.Write("xxxx")
} Magic food processor, from User C, who wants two laser
else { printers and 15 network cards for a total of US$894?
Response.Write("yyyy")
}
%> CGI programs usually solve the state problem using one of
two methods: expanded URLs that are generated dynami-
Here, the Response object is used to change the output based cally by the application, or cookies stored in the user’s
on some condition. cookie file. Both approaches are cumbersome. ASP uses
the cookies approach behind the scenes and exposes the
The Write method assumes you’re writing to an HTML state information using the Session object.
output (ContentType text/html) and will automatically
translate your strings to valid HTML representation, e.g. > While the Session object has some properties that will allow
will be translated to &gt;. If you want to write values that you to recognize the user, or to time out or abandon the
won’t be translated (for example, when creating a GIF session, its most important use is as a storage space for ses-
image), use the BinaryWrite method. sion information. Coming back to the shopping basket
problem, you could store the information about user C’s
The Server Object chosen products using the following:
The Server object has several utility functions useful to an
ASP application. HTMLEncode and URLEncode take plain Session("LaserPrinter") = 2;
string data and convert it to string data that can be Session("NetworkCards") = 15;
Session("FoodProcessor") = 0;
included in HTML source. For example:

<%= Server.HTMLEncode("A < 2") %> The Session object’s Session_OnStart event is called when a new
session is created. The code in this event can be used to create
will be translated to: session objects, or initialize variables used by the session. The
Session_OnEnd event is called when the session is terminated.
A &lt; 2
The Application Object
The browser will be able to display this code properly (A < The Application object is used to store information that is
2), where, if you wrote A < 2 in the source, the browser global in scope to the application, and is shared between

8 March 1999 Delphi Informant


On the Cover
all the users of an application. The Application object can I created five OleVariant variables to represent the main ASP
also be used to start Automation objects used across the objects, and I assigned them in this method:
application. The Application_OnStart event is activated
before the first Session is created, and can be used to ini- procedure TASPObject.OnStartPage(
tialize global application variables, or start global AScriptingContext: IUnknown);
begin
Automation objects. The Application_OnEnd event is the FScriptContext := AScriptingContext as IScriptingContext;
last event called when the last session used in the applica- FASPRequest := ScriptContext.Request;
tion quits. FASPResponse := ScriptContext.Response;
FASPSession := ScriptContext.Session;
FASPServer := ScriptContext.Server;
Delphi and ASP FASPApplication := ScriptContext.Application;
To write code that takes advantage of the ASP object end; // TASPObject.OnStartPage

model, you must start by importing the ASP type library


to Delphi. I use tlibimp.exe, which has been available in We can now create a simple HelloWorld procedure to test our
Delphi’s \bin directory since Delphi 3.02. Executing object (again, use the Type Library editor to add the method):
tlibimp.exe on the file ASP.dll, installed with ASP, results
in ASPTypeLibrary_TLB.pas and ASPTypeLibrary_TLB.dcr. procedure TASPObject.HelloWorld;
We’ll use the Pascal file in our Automation object. (You’ll begin
ASPResponse.Write('<h2>Hello World</h2>');
have to look for asp.dll on your hard disk. I found the ver- end; // TASPObject.HelloWorld
sion that came with the copy of PWS installation on
Windows 95 under C:\Windows\System\INetSrv). Once you
import the ASP type library, you’re ready to create your The code for our object is ready. We need to compile the project
Automation object with Delphi. and register the ActiveX server (Run | Register ActiveX Server).

Like every other Automation object, you’ll want to start by To test the object, I created a simple HelloWorld.asp, and
creating an ActiveX library; click on ActiveX Library on the installed it in a virtual directory defined in my Web server as
ActiveX page of the New Items dialog box (File | New). I DIASP. This directory must have read and script authorization.
saved my library as DIASP.dpr in my work directory. Now
add an Automation object to the project (also from the The code that calls our object from the ASP file is simple:
New Items dialog box). I gave the name ASPObject to the
new class, and saved the implementation unit as <%
ASPObj.pas. Set ASPObj = Server.CreateObject("DIASP.ASPObject")
%>
<H3>This page uses the ASP aware Delphi automation object </H3>
Every Automation object that wants direct access to the ASP <% ASPObj.HelloWorld %>
objects needs to add the ASPTypeLibrary_Tlb unit created
when we imported the ASP DLLs to the uses statement. We The result is shown in Figure 2. Notice that our Delphi
can now add a reference to a scripting context in the object code accessed the output stream directly via the ASP
definition. A scripting context is an ASP interface named Response object.
IScriptingContext that provides our object with access to the
ASP objects in the context of the session that uses the object. Why Use Delphi?
It’s obvious from this sample that it is easy to access the
Our class definition will now look like this:

type
TASPObject = class(TAutoObject, IASPObject)
private
FScriptContext: IScriptingContext;
public
property ScriptContext: IScriptingContext
read FScriptContext;
end;

When an Automation object is used by an ASP


application, the ASP engine checks if the object
implements the OnStartPage method before any
page processing is performed. If this method is
implemented, it is called and a scripting context is
passed to the object. From the Type Library editor,
I added a new OnStartPage method (the completed
type library is shown in Figure 1). Figure 1: Delphi’s Type Library editor.

9 March 1999 Delphi Informant


On the Cover

Figure 2: A simple HelloWorld.asp.

procedure TASPObject.VerifyCard;
var
VerifyObject: TCreditCardVerify;
begin
VerifyObject := TCreditCardVerify.Create(nil);
try
VerifyObject.CardNumber :=
ASPRequest.Form('CardNumber');
VerifyObject.SetCardTypeByName(
ASPRequest.Form('CardType'));
VerifyObject.SetExprDateFromStr(
ASPRequest.Form('ExprDate'));
Figure 4: After the verification component does its job, the
ASPResponse.Write('Checked Validity for Card Number ' +
VerifyObject.CardNumber + '<br>');
result is reported back using the ASP Response object.
case VerifyObject.Valid of
ccvValid : ASPResponse.Write('Card is valid!');
ccvExpired : ASPResponse.Write('Card Expired!'); facilities, and existing code modules over the primitive
ccvInvalid : ASPResponse.Write( tools available for script development.
'Card did not pass validation!');
end;
finally Let’s create, for example, a credit-card validation routine in
VerifyObject.free; Delphi. Assume your Web page receives credit-card orders,
end; and you need to verify that the credit-card number is valid.
end;
Usually, you’ll need some software that can connect to verify
the credit-card information supplied and use your merchant
Figure 3: The VerifyCard method uses the credit-card verifica- account, but for the purpose of this article, we’ll assume that
tion component. using the credit-card number validation algorithm is enough.
While this algorithm won’t rival the scheduling problems the
ASP objects from our Delphi code, but why would we NT operating system developers had to face, I wouldn’t want
even bother to do that if we can write the same code in to implement it in VBScript.
VBScript or JScript?
The CcardVer.pas unit defines a Delphi object
Using Delphi to interact directly with the ASP objects TCreditCardVerify that, based on the properties CardType,
provides several advantages. The first is speed; a compiled ExprDate, and CardNumber, will return the validity of the
Delphi object will be much faster than a script that needs card information. We will now add a new method to our
to be interpreted by the Active Script engine used by ASP. object that will access credit-card information sent from
If the logic you need to perform is complicated, your users an HTML form, verify it, and return a response to the
will receive better service if the calculations are performed user. (Don’t forget to use the Type Library editor to add
in a compiled module. The other advantage to using the new VerifyCard method.)
Delphi to write your logic is the power of the language,
and the ability to use existing code. I would not bother The code for the new method uses the credit-card verification
with Delphi code for simple ASP scripts, but when the component, as shown in Figure 3. This code uses the ASP
need for complicated logic or business rules arises, I would Request object to access the parameters passed from the
rather use Delphi’s strong development tools, debugging CreditCardVerify.html form. After the verification compo-

10 March 1999 Delphi Informant


On the Cover
nent does its job, the result is reported back using the ASP
Response object (see Figure 4).

Conclusion
Creating ASP-aware objects with Delphi is almost as easy as
creating any other kind of Automation object. If you are
writing ASP applications, Delphi code can be used to speed
calculations and development time.

Many tasks you’ve done in Delphi can now be exposed on


the Web using the best of both worlds: Delphi’s ease of code
development and performance with ASP’s easy deployment
and content authoring. If you have large amounts of Delphi
code that you need to Web-enable, and ASP is your Web
development technology of choice, the technique described
in this article will save you hours of porting.

More information about ASP can be found on Microsoft’s


Web site (http://www.microsoft.com), or as part of the
MSDN that ships with Visual InterDev or Visual Studio.
Many authoring tools support the creation of ASP pages.
Or, you can always fire up Notepad. ∆

The files referenced in this article are available on the Delphi


Informant Works CD located in INFORM\99\MAR\DI9903RL.

Ron Loewy is a software developer for HyperAct, Inc. He is the lead developer
of eAuthor Help, HyperAct’s HTML Help-authoring tool. For more information
about HyperAct and eAuthor Help, contact HyperAct at (515) 987-2910, or
visit http://www.hyperact.com.

11 March 1999 Delphi Informant


Patterns in Practice
Patterns / OOP / Persistence / Windows registry

By Xavier Pacheco

The Singleton Pattern


Implementing a Reusable Object for Saving Persistent Data

W e’re surrounded by patterns, both physical and behavioral. With the


exception of occasional acts of spontaneity, most people go about their
daily activities according to various behavioral patterns. Likewise, in software
development, many problems follow a common theme, and the majority of
these problems is solvable by applying object-oriented patterns. In a loose
sense, patterns are to object-oriented programming as algorithms are to struc-
tured programming.

Unlike our behavioral patterns, we can’t The Singleton Pattern


depend on programming patterns to pop into The Singleton pattern is a very simple pattern
our heads automatically. First, we must fully to implement. It’s used when you want to
understand the problem at hand. Second, we ensure that only one instance of a class exists.
must be able to determine that there is a pat- Additionally, the Singleton pattern is globally
tern that addresses the problem. Finally, we available throughout the application. Ideally, it
must implement a pattern that we’ve identi- can be overridden to allow programmers to
fied, modified, or created to solve the problem. extend the class without necessitating modifi-
cation to the client code. This last point is not
This is the first of a series of articles in which I always practical, but ideal if possible. Figure 1
will discuss patterns. Throughout this series, illustrates the structure of the Singleton class
I’ll give practical examples to implement the and its relationship to other classes.
patterns I discuss. The examples I’ll illustrate
may vary from simple Object Pascal usage, to There are several scenarios in which you
more complex implementations of might need to implement a Singleton pattern.
COM/DCOM and CORBA. My goals are to: The most common is to encapsulate a partic-
introduce you to common patterns or ular set of data and/or behaviors so they’re
variations thereof, available as one instance. This will ensure the
present practical solutions to common data isn’t replicated elsewhere in the applica-
problems using patterns, and tion, and changed so it becomes invalid. For
promote a frame of thinking when trying example, you might want to ensure that an
to solve common development problems. opened file is accessible from only one loca-
tion within your application.
TSingleton
Some common examples of Singletons used in
Singleton Return unique instance the VCL are the TApplication and TScreen class-
es that exist in all Delphi applications. Others
1 1 1
include TPrinter and TClipboard. I’ll illustrate
how to implement a Singleton for encapsulat-
1 1 1
ing application-wide user configuration data.
Class1 Class2 Class3

Implementing a Singleton Pattern Class


The following list specifies the steps
Figure 1: The Singleton class structure and its relations. required to implement a Singleton class:

12 March 1999 Delphi Informant


Patterns in Practice
1) Define private variables to ensure proper initialization for The finalization section of this unit takes care of freeing the
the Singleton class. TSingleton instance, if it hasn’t already been freed by checking if
2) Define an access function that returns the Singleton instance. FSingleton is not nil. FSingleton is set to nil in TSingleton.Destroy.
This function shall create the instance if it’s not already creat-
ed. Otherwise, it will return the previously created instance. TSingleton Usage
3) Override constructor to test if the object has already Using TSingleton is simple; it’s used similarly to the way
been created, or the constructor is called directly. If so, you use the TPrinter and TClipboard classes. For example,
raise an error. to invoke the TSingleton.ShowSingletonName method, sim-
4) Override the Singleton’s destructor to destroy a private ply refer to the access function as though it were the class,
instance, and set the Singleton variable to nil. which in effect it is, because it returns a reference to a
valid TSingleton instance:
In the following sections, I’ll demonstrate how to create a
skeleton Singleton class. Later, I’ll use this same skeleton to Singleton.ShowSingletonName;
solve a more practical problem.
The first time the client makes a reference to the Singleton
Defining the internal private variables. Listing One (on page function, the internal TSingleton instance is instantiated.
16) presents a skeleton Singleton class, TSingleton (this and all From this point, its existence will be present until the client
accompanying source code is available for download; see end of explicitly frees it, or until the application shuts down, at
article for details). Notice I’ve declared private variables in the which time the code in the finalization block is executed.
implementation section of this unit. The purpose of these vari-
ables is described in Figure 2. The client may also make reference using its own
TSingleton variable. This isn’t a problem because the
Defining the access function. The access function for the client’s variable and internal variable will be referring to
TSingleton class is defined as: the same instance. Even if the client frees its own variable
reference, another call to Singleton will simply cause the
function Singleton: TSingleton; internal instance to be instantiated again. Therefore, the
following code fragments are valid, even though they don’t
This function evaluates FSingleton <> nil to determine follow good programming practices. I use them here to
whether the class has already been instantiated. If the class make a point; I don’t really code like this:
isn’t created, the function creates it; otherwise, it simply
returns the reference to FSingleton. Notice that before call- var
ing the Create constructor for TSingleton, the function sets S: TSingleton;
begin
the FExternalCreation variable to False. This is how I S := TSingleton.Singleton;
enforce the rule that the client can’t directly call the S.ShowSingletonName;
TSingleton constructor. Notice that the Create constructor end;

raises an exception if FExternalCreation is True — the


default value for this variable. Therefore, you’ll see that the In the above code, not freeing the TSingleton instance is fine
only way to access the TSingleton class is to call the because the finalization block will free it when the application
Singleton function. shuts down. The following code won’t fail:

Another, and possibly easier, approach would be to make the var


constructor protected rather than public. I wanted to point S: TSingleton;
begin
out both options, and, because the latter is easier, I won’t S := Singleton;
illustrate it, other than to give it mention. S.Free;
Singleton.ShowSingletonName;
end;
Notice that I’ve also provided an alternate access method, a
class method defined as:
Variable Purpose
class function Singleton: TSingleton; FSingleton Private variable to refer to the
TSingleton instance. Client will not be
This method simply calls the access function, Singleton. I cre- able to directly access this variable.
ated this method to illustrate yet another technique for pro- FExternalCreation Boolean to indicate if the client
viding an access method. directly called the TSingleton con-
structor. A True value indicates an
Define constructor and destructor. As noted earlier, the con- error. Client must use the defined
structor evaluates the private variable FExternalCreation, and access function.
raises the appropriate exception. The destructor does the Figure 2: The variables declared in the implementation of the
reverse, and also sets the FSingleton variable to nil. TSingleton class.

13 March 1999 Delphi Informant


Patterns in Practice
Although the call to S.Free destroys the internal FSingleton type of store. As stated earlier, the TUserConfiguration class sim-
instance, the subsequent call to Singleton.ShowSingletonName plifies this by using the functionality provided by Delphi’s stream-
recreates that instance. The same goes for this code: ing system and RTTI. Data is made streamable by encapsulating
it as published properties of a TPersistent class descendant.
begin
Singleton.Free; The TStorage class handles the actual saving of the data.
Singleton.ShowSingletonName;
end;
TStorage is an abstract class. Two of its descendants,
TStorageCfg and TStorageReg, store the data in a configuration
file and system registry, respectively. TUserConfiguration uses
Now that I’ve shown you a generic Singleton class, I’ll show
the TStorage class to save/read user-configuration data.
you a more practical use for this class.
TUserConfiguration calls FStorage.SaveUserConf to save the
data, and FStorage.ReadUserConf to read it. TUserConfiguration
Introduction to TUserConfiguration doesn’t know the type of storage data is saved to, or read from;
I’ve worked on quite a number of projects involving user- it only knows that it uses the two abstract methods of the
interface design, and one requirement consistently arises: per- TStorage class. It’s the responsibility of the TStorage descendants
sistent user options. Although the options that end users to implement the saving and reading of data. (This brings up
want to save may vary, the concept is almost always the same. the topics of generalization and composition; please see my dis-
When users close their applications, some set of data is saved, cussion on these issues at the end of this article.)
so the next time the application is launched, those same
options are remembered. In many cases, these options have to TStorage defines two abstract methods: SaveUserConf and
do with security, e.g. the username, password, and perhaps a ReadUserConf. The SaveUserConf method is responsible for
list of accessible screens or routines. In other cases, it has to saving the user configuration information; the ReadUserConf
do with the user interface; things such as the main screen method is responsible for retrieving the information back
location and size might get saved along with other U/I fea- into the TUserConfiguration object.
tures, e.g. grid-column widths, pane sizes, fonts, etc.
Saving to a file. The TStorageCfg class implements the
There are many ways to implement this persistent data, and TStorage abstract methods to save and read data to and from a
because this is an article on Singleton classes, it seems fitting separate configuration file. We’ll handle this by using a
to illustrate how to implement such an object using the TFileStream object. TStorageCfg.SaveUserConf saves the data
Singleton pattern. I prefer the Singleton solution because it contained by TUserConfiguration to a file. In
allows the client application’s modules to access this data with TStorageCfg.SaveUserConf, the TFileStream.WriteComponent
the assurance that when the data is accessed and/or modified method writes a component and its streamable properties to
from one module, every module realizes the same data. the file created by the TFileStream.Create constructor. Because
TUserConfiguration is a TComponent descendant, its published
Listing Two (beginning on page 16) illustrates a simple imple- data, including other objects and their published data, get
mentation of the TUserConfiguration class. Although it con- stored to the file. TStorageCfg.ReadComponent does exactly the
tains quite a bit more code, it still follows the pattern shown opposite by calling TFileStream.ReadComponent from the file.
from the basic TSingleton skeleton, i.e. TUserConfiguration is
an extension of TSingleton. It contains four additional proper- Saving to the registry. The TStorageReg class is another
ties: UserName, Password, UserID, and MainScreenPos. The first implementation of TStorage that allows data to be saved to
three properties are simple data types, and MainScreenPos is of the registry. TStorageReg defines the field FRegKey that holds
type TMainScreenPos, a TPersistent descendant. MainScreenPos the location in the registry.
encapsulates four integer values into which I’ll store the bound-
aries for the main screen. At first glance, the abstract methods for TStorageReg seem
quite simple, as they both call a single procedure. The
I defined TMainScreenPos as a class because I wanted to illustrate TStorageReg.ReadUserConf method calls RegToComponentProps,
how you can store data in a hierarchical manner by taking whereas TStorageReg.SaveUserConf calls ComponentPropsToReg.
advantage of Delphi’s streaming mechanism and RTTI (run- The two procedures are utilities I’ve written to save and read a
time type information). Basically, I’m using the same system that component and its properties (including other objects) to and
stores your TForm properties when creating applications in from the registry. They are defined in the unit XWRegUtils.pas
Delphi. All TPersistent classes are streamable. The details of shown in Listing Three, beginning on page 18.
RTTI are beyond the scope of this article, but I’ll briefly summa-
rize where I use RTTI functionality in the TUserConfiguration Notice that both procedures, ComponentPropsToReg and
class. (If you’re interested in a more detailed discussion of RTTI, RegToComponentProps, make use of an internal procedure,
visit my Web site at http://www.xapware.com.) ProcessProps. Because the reading and writing of this data is
practically identical, I put it into a single procedure, and used a
Making TUserConfiguration Persistent Boolean parameter, AReadProps, to distinguish save and read
The intent of the TUserConfiguration class is to save data to some operations. This procedure walks through the published

14 March 1999 Delphi Informant


Patterns in Practice
classes’ internal methods and elements. The book
Design Patterns: Elements of Reusable Object-
Oriented Software [Addison-Wesley, 1994] by Erich
Gamma, et al. describes the inheritance model as
“white-box reuse.” By the way, if there is any single
book that should be read in regards to design pat-
terns, this is the one. Although it’s not specific to
Delphi, it gives an in-depth rundown of patterns as
they might be implemented in any language.

The inheritance model has its advantages. It allows


reuse by letting descendant classes take from the
Figure 3: Streamed data in the system registry. functionality of the ancestor class. Descendant class-
es are used to create “specialized” versions of the
(streamable) properties of a component, and writes them to the ancestor. In TUserConfiguration, for example, each descendant
registry. It also cleverly calls itself recursively to write published would need to override only the methods required for saving
objects to the registry in the same hierarchical fashion shown and reading data to and from its specific store.
in Figure 3. I won’t get into the details of how this procedure
works, because it has to do with RTTI and streaming. For There are two problems with the inheritance model. The first
now, you may be content in knowing that it’s very impressive. is that the class implementation used by the client is decided
upon at compile time. This isn’t a major problem because it’s
TUserConfiguration Miscellany possible to have the client refer to the ancestor rather than a
The rest of the code contained in the UserCfg.pas unit has to specific descendant. For example, TUserConfiguration might
do with the housekeeping of the TUserConfiguration class. As have two abstract methods for saving and reading data
you might expect, you’ll see code that creates and frees any that descendants would need to override. Clients would
internal classes accordingly, and initializes internal variables. code to TUserConfiguration directly. At some point, the
client application would still have to instantiate a special-
Take note of the TUserConfiguration.CreateStorage method. ized version of the ancestor.
This method creates the proper TStorage descendant, based
on the value of TUserConfiguration.FStorageType. The second, and more serious, problem has to do with the
CreateStorage is called from TUserConfiguration, the Create exposure of the ancestor class to its descendants. Because
constructor, and the setter method for the StorageType methods and internal fields are typically exposed to descen-
property, SetStorageType. dant classes, it’s difficult to change the implementation of the
parent class without forcing modifications to be made to its
Using the TUserConfiguration object is simple. Listing descendants. This is typical in cases where the descendant
Four (beginning on page 19) illustrates how to retrieve the class makes direct references to methods and fields of the par-
user data in the OnCreate event handler for the main ent class’ private/protected members.
form, and how to save this information in the OnClose
event handler. The OnChange event handlers for two By using the composition model, the specialized functionali-
TEdit components on the form change the UserName and ty is delegated to an entirely separate class. As with the
Password properties for TUserConfiguration. TUserConfiguration class, I’ve delegated the saving and
retrieving of data to the TStorage class. TUserConfiguration
Generalization or Composition refers to the interface defined for TStorage. I use the term
Earlier, while discussing how the TUserConfiguration class “interface” loosely here. This isn’t the same as an interface
worked with the TStorage class, I mentioned the issues of when dealing with COM; however, it’s similar. This model
generalization and composition, two methods of reusability
in object-oriented programming. I’m not going to get into AbstractClass
Primitive Method1
the details of these concepts here. You may visit my Web site
TemplateMethod PrimitiveMethod2
for further discussions on basic OOP concepts. My imple- Primitive Method1
mentation of TUserConfiguration is based on composition. I’ll PrimitiveMethod2

explain why I chose composition over generalization.

Generalization is the same as inheritance. Had I implemented


TUserConfiguration using strictly inheritance, it might have ConcreteClassA ConcreteClassB
looked like the example shown in Figure 4. In the inheritance
model, descendant classes take on the characteristics of their PrimitiveMethod1 PrimitiveMethod1
ancestor classes. Consequently, these descendant classes typi- PrimitiveMethod2 PrimitiveMethod2

cally have some, if not much, visibility into the ancestor Figure 4: TUserConfiguration based on the inheritance model.

15 March 1999 Delphi Informant


Patterns in Practice
more accurately incorporates reusability. Objects deal with
// Function entry point.
each other at the interface level. As long as you never change function Singleton: TSingleton;
the interface, objects are self-contained, and the dependen-
cies between classes are minimized. It becomes much easier implementation

to change the implementation of classes without necessitat- var


ing change to other classes used in the system. Additionally, // Private class variable.
client applications don’t need to know anything about the FSingleton: TSingleton = nil;
// Boolean valid creation indicator.
TStorage class. It just cares that TUserConfiguration can save FExternalCreation: Boolean = True;
and read data from a specified store.
// TSingleton.
constructor TSingleton.Create(AOwner: TComponent);
When using a composition model, you typically code to begin
interfaces. This requires that you design your classes cau- // Test if object has already been created.
tiously, because the interface of a class serves as a “contract” if FSingleton <> nil then
raise Exception.Create(
between it and other classes. In the next installment in this 'Singleton class already initialized.');
series, I’ll use this model to design an application frame- // Test if constructor was called external to the
work. You’ll be able to add modules to a shell application // Singleton() function.
if FExternalCreation then
without having to recompile it. raise Exception.Create(
'Call Singleton function to reference this class.');
Conclusion inherited Create(AOwner);
end;
We’ve discussed a practical use for the Singleton pattern by
illustrating a global user configuration object. We’ll be dis- destructor TSingleton.Destroy;
cussing more patterns and presenting real-world uses for begin
// Set the private variable to nil.
them. Programming patterns can consistently save you time; FSingleton := nil;
rather than trying to figure out how to solve a particular inherited Destroy;
problem, the solution may already be at hand in a pattern. end;

(I would like to thank Anne Pacheco and Steve Teixeira for procedure TSingleton.ShowSingletonName;
proofing this and many of my articles.) ∆ begin
ShowMessage(ClassName);
end;
The files referenced in this article are available on the Delphi
Informant Works CD located in INFORM\99\MAR\DI9903XP. class function TSingleton.Singleton: TSingleton;
begin
Result := Snglton.Singleton;
end;

function Singleton: TSingleton;


Xavier Pacheco is president and chief consultant for Xapware Technologies Inc., begin
where he provides enterprise-level consulting services and training. He is also the if FSingleton = nil then begin
co-author of Delphi 4 Developer’s Guide [SAMS Publishing, 1998]. You can write FExternalCreation := False;
try
Xavier at xavier@xapware.com, or visit his Web site at FSingleton := TSingleton.Create(nil);
http://www.xapware.com. finally
FExternalCreation := True;
end;
end;
Result := FSingleton;
end;
initialization
Begin Listing One — Snglton.pas
finalization
unit Snglton; // Free the global object, only if not freed previously.
if FSingleton <> nil then
interface FSingleton.Free;
end.
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs; End Listing One
type
// TSingleton class definition.
Begin Listing Two — UserCfg.pas
TSingleton = class(TComponent) unit UserCfg;
public
constructor Create(AOwner: TComponent); override; interface
destructor Destroy; override;
// Class function entry point. uses
class function Singleton: TSingleton; Windows, Messages, SysUtils, Classes, Graphics, Controls,
procedure ShowSingletonName; Forms, Dialogs;
end;

16 March 1999 Delphi Informant


Patterns in Practice
protected
type procedure SetHeight(const Value: Integer);
// Forward declarations. procedure SetLeft(const Value: Integer);
TStorage = class; procedure SetTop(const Value: Integer);
TMainScreenPos = class; procedure SetWidth(const Value: Integer);
TStorageType = (stConfigFile, stRegistry); published
property Left: Integer read FLeft write SetLeft;
// TUserConfiguration. property Top: Integer read FTop write SetTop;
TUserConfiguration = class(TComponent) property Width: Integer read FWidth write SetWidth;
private property Height: Integer read FHeight write SetHeight;
FStorage: TStorage; end;
FUserName: string;
FPassword: string; function UserConfiguration: TUserConfiguration;
FUserID: Integer;
FStorageType: TStorageType; implementation
FMainScreenPos: TMainScreenPos;
procedure SetMainScreenPos( uses
const Value: TMainScreenPos); XWRegUtils;
protected
procedure SetPassword(const Value: string); var
procedure SetUserName(const Value: string); FUserConfiguration: TUserConfiguration = nil;
procedure SetUserID(const Value: Integer); FExternalCreation: Boolean = True;
procedure SetStorageType(const Value: TStorageType);
procedure CreateStorage; virtual; function RemoveExt(const AFileName: string): string;
public begin
constructor Create(AOwner: TComponent; Result := Copy(AFileName, 1, Pos('.', AFileName)-1);
const AStorageType: TStorageType); end;
destructor Destroy; override;
procedure SaveUserConf; function UserConfiguration: TUserConfiguration;
procedure ReadUserConf; begin
property StorageType: TStorageType if FUserConfiguration = nil then begin
read FStorageType write SetStorageType; FExternalCreation := False;
class function UserConfiguration: TUserConfiguration; try
published FUserConfiguration :=
property UserName: string TUserConfiguration.Create(nil, stConfigFile);
read FUserName write SetUserName; finally
property Password: string FExternalCreation := True;
read FPassword write SetPassword; end;
property UserID: Integer read FUserID write SetUserID; end;
property MainScreenPos: TMainScreenPos Result := FUserConfiguration;
read FMainScreenPos write SetMainScreenPos; end;
end;
// TUserConfiguration.
// Base storage class. constructor TUserConfiguration.Create(AOwner: TComponent;
TStorage = class(TObject) const AStorageType: TStorageType);
private begin
procedure SaveUserConf(AUserConfiguration: if FUserConfiguration <> nil then
TUserConfiguration); virtual; abstract; raise Exception.Create(
function ReadUserConf(AUserConfiguration: 'UserConfiguration already initialized.');
TUserConfiguration): Boolean; virtual; abstract; if FExternalCreation then
end; raise Exception.Create(
'Call UserConfiguration to reference this class.');
// Configuration file storage. inherited Create(AOwner);
TStorageCfg = class(TStorage) FMainScreenPos := TMainScreenPos.Create;
procedure SaveUserConf(AUserConfiguration: FStorageType := AStorageType;
TUserConfiguration); override; CreateStorage;
function ReadUserConf(AUserConfiguration: ReadUserConf;
TUserConfiguration): Boolean; override; end;
end;
destructor TUserConfiguration.Destroy;
// Configuration file storage. begin
TStorageReg = class(TStorage) // Save the setting to storage.
FRegKey: string; SaveUserConf;
constructor Create; FStorage.Free;
procedure SaveUserConf(AUserConfiguration: FMainScreenPos.Free;
TUserConfiguration); override; FUserConfiguration := nil;
function ReadUserConf(AUserConfiguration: inherited Destroy;
TUserConfiguration): Boolean; override; end;
end;
procedure TUserConfiguration.CreateStorage;
// TMainScreenPos. begin
TMainScreenPos = class(TPersistent) if FStorage <> nil then
private FStorage.Free;
FHeight: Integer; case FStorageType of
FTop: Integer; stConfigFile: FStorage := TStorageCfg.Create;
FLeft: Integer; stRegistry: FStorage := TStorageReg.Create;
FWidth: Integer; end;

17 March 1999 Delphi Informant


Patterns in Practice
end; FName: string;
begin
procedure TUserConfiguration.ReadUserConf; FName := ChangeFileExt(Application.ExeName, '.dat');
begin FileStream := TFileStream.Create(FName, fmCreate);
if not FStorage.ReadUserConf(self) then begin try
FUserName := EmptyStr; FileStream.WriteComponent(AUserConfiguration);
FPassword := EmptyStr; finally
end; FileStream.Free;
end; end;
end;
procedure TUserConfiguration.SaveUserConf;
begin // TStorageReg.
FStorage.SaveUserConf(self); constructor TStorageReg.Create;
end; begin
inherited;
procedure TUserConfiguration.SetMainScreenPos( FRegKey := 'Software\' +
const Value: TMainScreenPos); RemoveExt(ExtractFileName(Application.ExeName));
begin end;
FMainScreenPos := Value;
end; function TStorageReg.ReadUserConf(
AUserConfiguration: TUserConfiguration): Boolean;
procedure TUserConfiguration.SetPassword( begin
const Value: string); RegToComponentProps(FRegKey, AUserConfiguration);
begin Result := True;
FPassword := Value; end;
end;
procedure TStorageReg.SaveUserConf(
procedure TUserConfiguration.SetStorageType( AUserConfiguration: TUserConfiguration);
const Value: TStorageType); begin
begin ComponentPropsToReg(AUserConfiguration, FRegKey);
FStorageType := Value; end;
CreateStorage;
ReadUserConf; // If the storage exists, read it. // TMainScreenPos.
end; procedure TMainScreenPos.SetHeight(const Value: Integer);
begin
procedure TUserConfiguration.SetUserID( FHeight := Value;
const Value: Integer); end;
begin
FUserID := Value; procedure TMainScreenPos.SetLeft(const Value: Integer);
end; begin
FLeft := Value;
procedure TUserConfiguration.SetUserName( end;
const Value: string);
begin procedure TMainScreenPos.SetTop(const Value: Integer);
FUserName := Value; begin
end; FTop := Value;
end;
class function TUserConfiguration.UserConfiguration:
TUserConfiguration; procedure TMainScreenPos.SetWidth(const Value: Integer);
begin begin
Result := UserCfg.UserConfiguration; FWidth := Value;
end; end;

// TStorageCfg. initialization
function TStorageCfg.ReadUserConf(AUserConfiguration:
TUserConfiguration): Boolean; finalization
var // Free the global object, only if not freed previously.
FileStream: TFileStream; if FUserConfiguration <> nil then
FName: string; FUserConfiguration.Free;
begin end.
Result := False;
FName := ChangeFileExt(Application.ExeName, '.dat');
if FileExists(FName) then begin End Listing Two
FileStream := TFileStream.Create(FName, fmOpenRead);
try
FileStream.ReadComponent(AUserConfiguration);
Begin Listing Three — XWRegUtils.pas
Result := True; unit XWRegUtils;
finally
FileStream.Free; interface
end;
end; uses Classes;
end;
// Saves the component specified by AComponent and its
procedure TStorageCfg.SaveUserConf(AUserConfiguration: // properties to the system registry in the location
TUserConfiguration); // specified by AregKey.
var procedure ComponentPropsToReg(AComponent: TComponent;
FileStream: TFileStream; const ARegKey: string);

18 March 1999 Delphi Informant


Patterns in Practice
// Reads the component properties for the component const ARegKey: string);
// specified by AComponent from the system registry at the var
// location specified by AregKey. RegIni: TRegIniFile;
procedure RegToComponentProps(const ARegKey: string; begin
AComponent: TComponent); RegIni := TRegIniFile.Create(ARegKey);
try
implementation ProcessProps(AComponent, EmptyStr, False, RegIni);
finally
uses TypInfo, Registry, SysUtils; RegIni.Free;
end;
procedure ProcessProps(AObject: TObject; end;
const ARegSection: string; AReadProps: Boolean;
ARegIni: TRegIniFile); procedure RegToComponentProps(const ARegKey: string;
var AComponent: TComponent);
PropList: PPropList; var
TypeData: PTypeData; RegIni: TRegIniFile;
i: Integer; begin
PropName: string; RegIni := TRegIniFile.Create(ARegKey);
TempObject: TObject; try
begin ProcessProps(AComponent, EmptyStr, True, RegIni);
TypeData := GetTypeData(AObject.ClassInfo); finally
RegIni.Free;
if TypeData.PropCount <> 0 then begin end;
GetMem(PropList, SizeOf(PPropInfo)*TypeData.PropCount); end;
try
GetPropInfos(AObject.ClassInfo, PropList); end.
for i := 0 to TypeData.PropCount - 1 do begin
PropName := PropList[i]^.Name;
// Filter out published properties of TComponent. End Listing Three
if not ((PropName = 'Name') or

begin
(PropName = 'Tag')) then Begin Listing Four — MainFrm.pas
// For now, only process integers, strings, and unit MainFrm;
// other objects.
if AReadProps then interface
case PropList[i]^.PropType^.Kind of
tkInteger: uses
SetOrdProp(AObject, PropList[i], Windows, Messages, SysUtils, Classes, Graphics, Controls,
ARegIni.ReadInteger(ARegSection, Forms, Dialogs, StdCtrls;
PropName, 0));
tkString, tkLString: type
SetStrProp(AObject, PropList[i], TMainForm = class(TForm)
ARegIni.ReadString(ARegSection, edtUserName: TEdit;
PropName, EmptyStr)); edtPassword: TEdit;
tkClass: begin lblUserName: TLabel;
TempObject := TObject(GetOrdProp( lblPassword: TLabel;
AObject, PropList[i])); procedure FormClose(Sender: TObject;
ProcessProps(TempObject, PropName, var Action: TCloseAction);
True, ARegIni); procedure FormCreate(Sender: TObject);
end; procedure edtUserNameChange(Sender: TObject);
end procedure edtPasswordChange(Sender: TObject);
else end;
case PropList[i]^.PropType^.Kind of
tkInteger: var
ARegIni.WriteInteger(ARegSection, MainForm: TMainForm;
PropName, GetOrdProp(AObject,
PropList[i])); implementation
tkString, tkLString:
ARegIni.WriteString(ARegSection,PropName, uses UserCfg;
GetStrProp(AObject, PropList[i]));
tkClass: begin {$R *.DFM}
TempObject := TObject(GetOrdProp(AObject,
PropList[i])); procedure TMainForm.FormCreate(Sender: TObject);
ProcessProps(TempObject, PropName, False, begin
ARegIni); with UserConfiguration do begin
end; // Change the storage location to the system registry.
end StorageType := stRegistry;
end; if not ((MainScreenPos.Width = 0) or
end; (MainScreenPos.Height = 0)) then
finally SetBounds(MainScreenPos.Left, MainScreenPos.Top,
FreeMem(PropList, MainScreenPos.Width, MainScreenPos.Height);
SizeOf(PPropInfo) * TypeData.PropCount); edtUserName.Text := UserName;
end; edtPassword.Text := Password;
end; end;
end; end;

procedure ComponentPropsToReg(AComponent: TComponent; procedure TMainForm.FormClose(Sender: TObject;

19 March 1999 Delphi Informant


Patterns in Practice
var Action: TCloseAction);
begin
with UserConfiguration do begin
MainScreenPos.Left := Left;
MainScreenPos.Top := Top;
MainScreenPos.Width := Width;
MainScreenPos.Height := Height;
end;
end;

procedure TMainForm.edtUserNameChange(Sender: TObject);


begin
UserConfiguration.UserName := edtUserName.Text;
end;

procedure TMainForm.edtPasswordChange(Sender: TObject);


begin
UserConfiguration.Password := edtPassword.Text;
end;

end.

End Listing Four

20 March 1999 Delphi Informant


Multi-Tier
Delphi Enterprise / MIDAS / Multi-tier Architecture

By Bill Todd

The Briefcase Model


When Your Application Must Travel Well

O ne of the benefits of using the MIDAS multi-tier application architecture is


the ability to build briefcase-model applications. A briefcase-model appli-
cation is a multi-tier application that allows the user of the client applica-
tion to save a set of records in local files, disconnect from the network that
hosts the application server and database, and edit the data off-line. Later,
the user reconnects to the network, and applies any accumulated updates
to the database.

There are two architectures that can form the There are two ways to handle this situation.
basis for a briefcase-model application. The The server can attempt to connect to the
first is the classic multi-tier application, database, and, if it fails, can notify the client
where the client application runs on one that the database isn’t available. This is by far
machine. The application server (the middle the more complex solution, because the client
tier) runs on another machine on the net- will have to call a custom method on the
work, and the database server runs on yet server to determine if the database is avail-
another machine. In this case, a MIDAS able, and, if it isn’t, modify its behavior to
license is required. In a briefcase application, work with local data, even though the appli-
when the client application starts, it must cation server is running.
determine if the application server is avail-
able. The easy way to see if the server is avail- A much simpler approach is to assume the
able is to set the Connected property of the user knows whether she or he is connected
connection component in the client applica- to the network, and provide two icons to
tion to True. If the server isn’t available, an start the client application. One icon is
exception is raised. used when connected to the network, and
the other when not connected. The icon
One of the nice things about the MIDAS that starts the client when the network isn’t
licensing requirements is that you can build available can include a command-line para-
briefcase-model applications without having meter that tells the client to run in brief-
to purchase a MIDAS license. As long as the case mode.
client application and the application server
that provides its data run on the same A Sample Application
machine, no license is required. The sample application accompanying this
article (see Figure 1) demonstrates these
This architecture complicates a briefcase techniques. (The client and server projects
application because the application server is discussed in this article are available for
always available. Starting the client when not download; see end of article for details.) To
connected to the network will cause the run the sample application, compile and
application server to start, but the server run the EbSrvr server first, so it will register
won’t be able to connect to the database. itself as an Automation server.

21 March 1999 Delphi Informant


Multi-Tier

{ If the -L command-line parameter is not present, try to


connect to the application server. If the connection
succeeds, set the global variable IsConnected to True. }
procedure TEcMainForm.ConnectToServer;
begin
{ If the local command-line parameter is present,
don't try to connect to the server. }
if ParamCount > 0 then
if UpperCase(ParamStr(1)) = '-L' then begin
DisableServerFeatures;
Exit;
end; // if
{ Try to connect to the server. If the connection
is established, set IsConnected to True; }
with MainDm do
try
EbConn.Connected := True;
IsConnected := True;
except
DisableServerFeatures;
Exit;
end; // try
Figure 1: The demonstration client and server application at
{ If the server is available, see if the database is.
run time.
If not, shut down the server. }
with MainDm do
if not EbConn.AppServer.IsDatabase then begin
... IsConnected := False;
{ Try to connect to the server. } EbConn.Connected := False;
ConnectToServer; end; // if
with MainDm do begin end;
{ If the server is not available, and there is no local
data to load, notify the user and terminate. }
if (LoadLocalData = False) and
Figure 3: The ConnectToServer method.
(IsConnected = False) then begin
MessageDlg('There is no data available.', Calling SaveToFile saves both the Data and Delta properties
mtInformation, [mbOK], 0);
Application.Terminate;
of the TClientDataSet. That is, both the data and the changes
Exit; that have been made — but not applied to the database —
end; // if are saved. This allows you to edit off-line and resave the origi-
{ Open the client datasets. }
CustomerCds.Open;
nal data and unapplied changes as many times as you wish.
OrderCds.Open;
... Starting the Client
Figure 2: Determining if the server is available.
When the client starts, it must determine if the server is avail-
able. This is handled in the main form’s OnCreate event han-
dler, part of which is shown in Figure 2. This code must deal
Saving Data with three possible conditions upon application startup:
The first requirement of a briefcase client application is the 1) Local data is available.
ability to save the data that will be used off-line in local files. 2) Local data is not available, but the application server is
The SaveToFile method of TClientDataSet provides this abili- available.
ty. The following code is from the Save To Local Drive menu 3) Neither local nor server data is available.
choice’s OnClick event handler:
If local data is available, it must be loaded. If the application
procedure TEcMainForm.SaveLocally1Click(Sender: TObject); server is available, a connection to it must be established. If
begin no data was loaded from local files, data will be fetched from
with MainDm do begin the server automatically when the client datasets are opened.
CustomerCds.SaveToFile(CustomerFileName);
OrderCds.SaveToFile(OrderFileName);
If no data is available from local files or the server, the user
AllOrdersCds.SaveToFile(AllOrderFileName); must be warned, and the application terminated.
end;
end;
The code in the OnCreate event handler begins by calling the
ConnectToServer method, shown in Figure 3. The main form’s
This code calls the SaveToFile method of each of the client unit includes a global variable, IsConnected, which is initial-
dataset components in the program’s data module. ized to False. The method then sets that variable to True if a
SaveToFile’s parameter is the name of the file to which to connection can be established to the application server.
save the data. The file can have any name and file extension
you choose. In this case, the file names are defined as global The ConnectToServer method begins by checking if any
constants in the implementation section of the main form’s command-line parameters are present. If so, it checks if
unit, so they can be easily changed if necessary. the first parameter is -L. If the -L parameter is present,

22 March 1999 Delphi Informant


Multi-Tier

{ Try to load local data files. If they exist,


procedure TEcMainForm.ApplyChanges1Click(Sender: TObject);
return True; otherwise, return False; }
begin
function TEcmainForm.LoadLocalData: Boolean;
with MainDm do begin
begin
with CustomerCds do begin
Result := False;
{ If there is an unposted record, post it. }
with MainDm do begin
if State in [dsEdit, dsInsert] then
{ If local files exist, load them. }
Post;
if FileExists(CustomerFileName) then begin
{ If there are changes, apply them. If the changes
CustomerCds.LoadFromFile(CustomerFileName);
are applied successfully, refresh. }
Result := True;
if ChangeCount > 0 then
end; // if
if ApplyUpdates(ChangeCount) = 0 then
if FileExists(OrderFileName) then
Refresh;
OrderCds.LoadFromFile(OrderFileName);
end; // with CustomerCds
if FileExists(AllOrderFileName) then
with OrderCds do begin
AllOrdersCds.LoadFromFile(AllOrderFileName);
{ If there is an unposted record, post it. }
end; // with
if State in [dsEdit, dsInsert] then
end;
Post;
{ If there are changes, apply them. If the changes
Figure 4: The LoadLocalData method. are applied successfully, refresh. }
if ChangeCount > 0 then
DisableServerFeatures is called, and this method exits, leav- if ApplyUpdates(ChangeCount) = 0 then
Refresh;
ing IsConnected set to its default value of False. If the -L end; // with OrderCds
command-line parameter isn’t present, the TDCOMConnection end; // with MainDm
component’s Connected property is set to True inside a { If local files exist, delete them now that the updates
have been applied. }
try..except block. If the server can’t be started, an excep- if FileExists(CustomerFileName) then
tion will occur. If the connection is established, the DeleteFile(CustomerFileName);
IsConnected variable is set to True. If the connection fails, if FileExists(OrderFileName) then
DeleteFile(OrderFileName);
IsConnected retains its default value of False, and if FileExists(AllOrderFileName) then
DisableServerFeatures is called. DeleteFile(AllOrderFileName);
end;

If a connection to the application server is established, the


Figure 5: Applying updates and deleting the local files.
application server’s IsDatabase method is called. This method
attempts to set the Connected property of the server’s Fourth is the FilterLabel component, which shows
TDatabase component to True. If the connection succeeds, whether the filter on the server is currently enabled on
IsDatabase returns True; otherwise, it returns False. If the the server. This is set by a callback from the server, and
database isn’t available, the application server is closed, and isn’t available without the server.
IsConnected is set back to False.

The DisableServerFeatures method: The next block of code in the main form’s OnCreate event
handler attempts to load any data in local files by calling
{ Disable features that are only available when the LoadLocalData method, shown in Figure 4. This
connected to the application server. } method returns True if local data is found. The method
procedure TEcMainForm.DisableServerFeatures;
begin also checks if each file exists, and if so, calls the corre-
View1.Enabled := False; sponding client dataset’s LoadFromFile method to load the
ApplyChanges1.Enabled := False; data. In this application, there is no way to save order data
FilterStringLabel.Visible := False;
FilterLabel.Visible := False; without saving customer data, so the return value is set to
end; True if the local customer file is found.

takes care of disabling features of the client application that Reconnecting


won’t work if the application server isn’t running. In this There is one more alteration to the client required in a
application, there are four features that must be disabled: briefcase-model application. When the user reconnects to
The first is the View menu choice, which allows the client the network after making off-line changes, and applies
to set a filter on the server that displays all customers, or those changes to the database, the local files remain. The
only customers in the US. next time the client is started, it will reload the same local
The second is the Apply Changes choice on the File menu, data and changes. To prevent this, the code for the Apply
which calls ApplyUpdates for each of the client datasets. Changes menu choice is changed to delete the local files
The third is the FilterStringLabel component, which after updates are applied successfully (see Figure 5).
shows the filter expression on the server. While this
expression could still be displayed (because it’s includ- In Case They Forget ...
ed in the data packets), there’s no way to know if the There’s one more change that’s not required (except perhaps
filter was enabled at the time the data was saved to the to the client). It’s possible the user could make changes off-
local files. line and forget to save them before closing the program. To

23 March 1999 Delphi Informant


Multi-Tier
prevent this, save the files in the main form’s OnClose event
handler, as shown here:

procedure TEcMainForm.FormClose(Sender: TObject;


var Action: TCloseAction);
begin
if not IsConnected then
SaveLocally1Click(Self);
end;

Of course, there’s more to any briefcase-model applica-


tion. Please give the sample application a spin, and study
it, before creating your own briefcase solutions. ∆

The files referenced in this article are available on the Delphi


Informant Works CD located in INFORM\99\MAR\DI9903BT.

Bill Todd is president of The Database Group, Inc., a database consulting and
development firm based near Phoenix. He is a Contributing Editor of Delphi
Informant, co-author of four database-programming books, author of over 60
articles, and a member of Team Borland, providing technical support on the
Inprise Internet newsgroups. He is a frequent speaker at Inprise conferences in
the US and Europe. Bill is also a nationally known trainer and has taught
Paradox and Delphi programming classes across the country and overseas. He
was an instructor on the 1995, 1996, and 1997 Borland/Softbite Delphi World
Tours. He can be reached at bill@dbginc.com or (602) 802-0178.

24 March 1999 Delphi Informant


Undocumented
Win32 API / Delphi 3

By Kevin J. Bluck and James Holderness

Shell Notifications
Getting Windows to Share Some of Its Secrets

Y ou may have stumbled across the Windows API function SHChangeNotify.


According to the Windows docs, it notifies the system of any events that
may affect the shell. Well, that’s very nice for the shell. There are many inter-
esting events the system monitors: file and directory changes, media insertion
and removal, disk free-space updates, etc. For example, you’re probably famil-
iar with having Windows Explorer update the CD drive’s icon when you insert
or remove a CD. That’s an instance of shell notifications at work. Very useful,
but wouldn’t it be nice if you could see those notifications as well?
Until now, tapping into this mechanism largely opaque; they weren’t intended for
required insider knowledge. For some reason, direct display or manipulation.
Microsoft decided not to reveal the methods
by which Windows Explorer receives these One tricky aspect of PIDLs is that they must
notifications. This article exposes those often be allocated in one module, and freed in
secrets, and provides a nifty component to a module written by a different party. This can
give you a head start in using this exciting be problematic, as different development envi-
technique the Delphi way. ronments often use different memory alloca-
tion schemes. For example, using Delphi’s
A Brief Digression FreeMem procedure to free memory originally
Before getting wrapped up in the details of allocated by some C compiler’s malloc RTL
signing up for shell notification, you need to function would most likely end up corrupting
know just a little bit about the PItemIDList the heap. As a result, the memory buffers to
record type. This is a type defined in the stan- contain PIDLs must be allocated and freed by
dard ShlObj unit, and refers to the construct the shell task allocator. This ensures the PIDL’s
known to shell programmers as a PIDL (pro- memory will always be allocated and freed
nounced “piddle”). The nature of PIDLs is a using the same scheme, regardless of the devel-
broad topic, more than enough to occupy its opment environment used for the module.
own article. For the purposes of shell notifica- This functionality is implemented through a
tions, however, a deep exposure isn’t necessary. COM interface named IMalloc. Using any-
thing but this global allocation engine to allo-
Basically, a PIDL is the shell version of the cate and free PIDLs is a quick route to an
DOS path. If you look at the folder tree in abnormal termination. You can use the
the left pane of Windows Explorer, you can IMalloc interface directly for this, but we have
see all the file system folders. You can also see opted instead to use some “cheater” functions,
folders that aren’t part of the file system, such which you’ll find defined in the kbsnPIDL
as Control Panel. Some means was necessary unit in the sample files included with this arti-
to identify all folders uniquely, whether they cle. (All source in this article is available for
were part of the file system or not. download; see end of article for details.)
Microsoft’s solution was the PIDL, a sort of
turbo-charged path. It’s not a simple string; The upshot of all this is that every file system
rather, it’s a pointer to a chain of structures object can be represented either as a PIDL or
that contain identifying information for a a path. In addition, many non-file system
given folder. The contents of a PIDL are objects also exist that can’t be identified by

25 March 1999 Delphi Informant


Undocumented
Constants associated with single events
SHCNE_ASSOCCHANGED A file-type association has changed.
SHCNE_ATTRIBUTES The attributes of an item or folder have changed.
SHCNE_CREATE A non-folder item has been created.
SHCNE_DELETE A non-folder item has been deleted.
SHCNE_DRIVEADD A drive has been added.
SHCNE_DRIVEADDGUI A drive has been added via the shell.
SHCNE_DRIVEREMOVED A drive has been removed.
SHCNE_EXTENDED_EVENT Not currently used.
SHCNE_FREESPACE The amount of free space on a drive has changed.
SHCNE_MEDIAINSERTED Storage media has been inserted into a drive.
SHCNE_MEDIAREMOVED Storage media has been removed from a drive.
SHCNE_MKDIR A folder has been created.
SHCNE_NETSHARE A folder on the local computer is being shared via the network.
SHCNE_NETUNSHARE A folder on the local computer is no longer being shared via the network.
SHCNE_RENAMEFOLDER The name of a folder has changed.
SHCNE_RENAMEITEM The name of a non-folder item has changed.
SHCNE_RMDIR A folder has been removed.
SHCNE_SERVERDISCONNECT The computer has disconnected from a server.
SHCNE_UPDATEDIR The contents of an existing folder changed, but the folder wasn’t renamed.
SHCNE_UPDATEIMAGE An image from the system image list has changed.
SHCNE_UPDATEITEM An existing non-folder item changed, but the item wasn’t renamed.
Constants that combine multiple event types
SHCNE_ALLEVENTS Specifies a combination of all possible event identifiers.
SHCNE_DISKEVENTS Specifies a combination of all of the disk event identifiers.
SHCNE_GLOBALEVENT Specifies a combination of all of the global event identifiers.
Flag used with event constants
SHCNE_INTERRUPT The event occurred as a result of a system interrupt.
Figure 1: Shell event constants.
anything but a PIDL. Many shell functions, therefore, require invisible window whose only responsibility is handling the
PIDLs as parameters instead of traditional paths, or return notification messages.
PIDLs allocated from within the shell function that you may
need to free later. For the purposes of this article, you may The EventMask parameter is a bit-mask of all the events you
consider a PIDL to be a pointer — supplied by the shell — are interested in. You can use any combination of the
that points to arbitrary data that should not be modified in SHCNE_xxx constants, the same ones that are used for the
any way. Functions have been provided in unit kbsnPIDL SHChangeNotify function, combined with a logical or opera-
that will convert a file system path to a PIDL, and vice versa. tion. Figure 1 provides a complete listing of these constants.
The only unusual aspect is that you should never free a PIDL
with the usual VCL functions, such as FreeMem; you must The Flags parameter allows you to specify optional behavior for
use only the FreePIDL function provided in unit kbsnPIDL. the notifications. You may filter out interrupt or non-interrupt
events, and decide whether to use a proxy window under NT.
Getting “In the Loop” See Figure 2 for a list of these flags. Typically, both interrupt
The key to receiving shell change notifications is the and non-interrupt flags should be set, as you generally don’t
SHChangeNotifyRegister function. Here’s its prototype: care about the ultimate source of the event. At any rate, inter-
rupt events are extremely rare. The SHCNF_NO_PROXY flag
function SHChangeNotifyRegister(Window: HWND; Flags: DWORD; allows you to handle the notification more efficiently on
EventMask: ULONG; MessageID: UINT; ItemCount: DWORD; Windows NT, but it complicates the message handling proce-
var Items: TNotifyRegister): THandle; stdcall;
dure, and requires a couple more undocumented functions.
We’ll explain the whole situation with NT later.
It’s used to register a window with the shell, which will then
be notified of all subsequent SHChangeNotify events. It’s The MessageID parameter is the identifier of the message that
exported from SHELL32.DLL. Like most undocumented will be sent to that window. It’s recommended you use a value
functions, it’s not exported by name. Therefore, it’s necessary derived from WM_USER for the value of MessageID to avoid
to link using the function ordinal. The export ordinal for
SHChangeNotifyRegister is 2. Flag Value
SHCNF_ACCEPT_INTERRUPTS $0001
The Window parameter specifies the handle of the window SHCNF_ACCEPT_NON_INTERRUPTS $0002
that should receive the notification messages. This can be SHCNF_NO_PROXY $8000
any window you desire, but it’s usually best to create an Figure 2: SHChangeNotifyRegister flags.

26 March 1999 Delphi Informant


Undocumented
conflicts with system messages. If you use a single-purpose This is a variant record type, but you should consider it to
window as previously described, the value of WM_USER itself be defined as shown here:
is fine. The details of the message handling are explained later.
TMessage = record
The ItemCount parameter specifies the number of paths you Msg: DWORD;
WParam: DWORD;
wish to monitor. Usually, this will be 1, but it’s possible to LParam: DWORD;
monitor many different paths via the Items parameter. Result: DWORD);
end;

The Items parameter is a pointer to a record of type


TNotifyRegister. The following is the definition of this record type: The Msg data member of the message record will be set to
whatever value you specified in the MessageID parameter
TNotifyRegister = packed record you passed to the SHChangeNotifyRegister function. Use
pidlPath: PItemIDList; this parameter to recognize notification messages, as
bWatchSubtree: BOOL;
end; opposed to the myriad other miscellaneous messages the
Windows system will send to your window.

If the pidlPath data member of this record specifies a valid The LParam data member will be set to the ID of the event
PIDL for a valid folder, you’ll receive events that affect the that occurred. This will be one of the SHCNE_xxx values
folder itself, as well as any items in the folder. If you set the shown in Figure 1. It’s possible that multiple event IDs
bWatchSubtree data member to True, you’ll receive events for could be or’ed together, so it would be best to test for the
the entire sub-tree rooted at the specified folder, e.g. all fold- presence of a given ID value, using a logical and operation,
ers and items below the specified folder in addition to the rather than via an equality test using the = operator.
folder itself. If you set the pidlPath data member to nil, you’ll
receive events for every folder and item on the system. If the The WParam will be a pointer to a record of type
value of the ItemCount parameter is greater than 1, you must TTwoPIDLArray. The name WParam, which is C-style short-
supply the same number of TNotifyRegister records to the hand for Word Parameter, is an anachronism. It’s now actual-
Items parameter in the form of a vector, one record packed ly a 32-bit long type, just like LParam, and so can store a
after another into a buffer large enough to hold all of them. pointer. This record points to an array containing the two
PIDLs associated with the event, as shown here:
If the SHChangeNotifyRegister function succeeds, the return
value is a handle of a shell change notification object. You TTwoPIDLArray = packed record
should save this handle for later use. If the function fails, the PIDL1: PItemIDList;
PIDL2: PItemIDList;
return value is 0. end;

Shut It Off Those of you familiar with the SHChangeNotify function may
When you’re finished monitoring the notification events,
be wondering why you only receive PIDLs from shell change
you should pass the notification handle returned by
notification messages, even though the SHChangeNotify func-
SHChangeNotifyRegister to SHChangeNotifyDeregister. The
tion accepts PChar and DWORD data types, as well as PIDLs.
export ordinal value of SHChangeNotifyDeregister is 4, and
The reason is that all the data types are automatically convert-
the function declaration is as follows:
ed to PIDLs by the shell before being sent anywhere. For
SHCNF_PATH and SHCNF_PRINTER types, this seems
function SHChangeNotifyDeregister(Notification: THandle):
BOOL; stdcall; obvious enough. For the SHCNF_DWORD type, a 10-byte
“fake” PIDL is created, with the two DWORD items immedi-
The Notification parameter takes the handle of a shell ately following the cb data member. Ignore the cb data mem-
change notification object returned by a successful call to ber; it has no use in this situation except as a placeholder. This
SHChangeNotifyRegister. As you might guess, if the func- encoding scheme is encapsulated for your convenience by the
tion succeeds, the return value is True; if it fails, the return TDWORDItemID record type, as shown here:
value is False.
TDWORDItemID = packed record
Getting the Message cb: Word; { Ignore }
dwItem1: DWORD;
After registering to receive shell notification messages, the dwItem2: DWORD;
shell will send its notification messages to the window you end;
specified in the Window parameter of the
SHChangeNotifyRegister function. You must then crack the The SHCNE_FREESPACE event is handled as a special case.
relevant data out of the message to get useful information. When the drive is passed in as a path or a PIDL, it’s converted to
Delphi doesn’t define a special message record type for a DWORD contained by the above encoding scheme, with dri-
these messages, so simply use the standard TMessage type. ves A: to Z: mapping onto bits 0 to 25. For example, drive D:

27 March 1999 Delphi Informant


Undocumented
would map to bit 3, so the value of the dwItem1 data member record that contains the two PIDLs for the notification mes-
would have a value of 8. If there are two drives specified for the sage, which are normally passed via the WParam data member
event, then two bits will be set in the DWORD. The value of of the message. Don’t try to free this pointer directly, either.
the second DWORD in this case appears to be meaningless.
The EventID parameter is also output-only, and takes a vari-
Windows NT and Memory Maps able of type ULONG, or Longint, if you prefer. You may ini-
This all seems simple enough, at least by the standards of tialize this variable to 0 before calling the function. When the
Windows shell programming, but on Windows NT, there is a function returns, this variable will contain the EventID of the
bit of a problem. NT maintains a careful separation of mem- event for this message, which is normally passed by the
ory used by different processes. One process attempting to LParam data member of the message.
directly access memory owned by another process puts you in
the express lane to your favorite exception, the General The return value is a handle to the memory map, which you
Protection Fault. You can’t simply send a message to a win- should save; you’ll need it later. If by some strange circum-
dow in a different process, and still expect the structure stance the function should fail, it returns a 0.
pointer contained in that message to be accessible.
When you have finished working with the data extracted via
To get around this, NT actually dumps all the relevant data into SHChangeNotification_Lock, you should unlock the memory
a memory-mapped file, which is accessible from any process, map so NT can properly dispose of it. This is the purpose of
then sends the memory map handle and a process ID as the the SHChangeNotification_Unlock function. The export ordi-
parameters to the message. To remain compatible with Windows nal value of SHChangeNotification_Unlock is 645, and the
95, somebody obviously has to extract the information from that function declaration is as follows:
memory map on the other side. The way this works is that NT
automatically creates a hidden “proxy” window whenever you function SHChangeNotification_Unlock(Lock: THandle):
call SHChangeNotifyRegister. It is the proxy window that receives BOOL; stdcall;

the notification message containing the memory map. Its mes-


sage handler then extracts all the information, and passes on the The Lock parameter is the handle you obtained from the call
correct message with the expected data to your window. to the SHChangeNotification_Lock function. The function
returns True if successful, and False on failure.
Of course, this is not exactly efficient, which is where the
SHCNF_NO_PROXY flag comes in. By specifying that flag It’s important to note that these functions exist only in
when calling SHChangeNotifyRegister, you’re telling NT to Windows NT. If you attempt to link to them while running
not create the proxy window, so the memory map handle gets Windows 95, you’ll experience a link failure. Therefore, it’s
passed directly to your window in the notification message. impossible to use the Delphi external method for linking,
It’s then up to you to extract the relevant information from unless you are completely sure your program will never run
the memory map. Fortunately, there are two functions that on anything but NT. You should use dynamic linking instead
do all the work for you: SHChangeNotification_Lock and by calling the GetProcAddress Windows API function after
SHChangeNotification_Unlock. testing which operating system is running. See Figure 3 for
an example of using these NT mapping functions.
The export ordinal value of SHChangeNotification_Lock is
644, and the function declaration is shown here: The Origin of Events
So, now you know how to receive all these shell notifications
function SHChangeNotification_Lock(MemoryMap: THandle; that are floating around, but who is actually generating them?
ProcessID: DWORD; var PIDLs: PTwoPIDLArray; According to the Windows documentation, “An application
var EventID: ULONG): THandle; stdcall;
should use this function (SHChangeNotify) if it performs an
action that may affect the shell.” That seems to be a bit of
The MemoryMap parameter is a handle to a chunk of memory wishful thinking. We can’t imagine there are many applica-
allocated by the NT system. This handle will be contained in tion developers who really give a hoot whether the shell is
the WParam data member of the shell notification message. kept informed of their actions.

The ProcessID parameter takes the ID of the process that gen- Fortunately, the shell seems to generate most of the notifications
erated the memory map. This value is contained in the itself. Sometimes, it may be directly responsible for an event, in
LParam data member of the shell notification message. which case, it’s easy enough for it to make the call to
SHChangeNotify. However, for things likely to originate in
The PIDLs parameter is output-only, and takes a variable of another application, such as file creation, it would presumably
type pointer to a record of type TTwoPIDLArray. You may ini- have to be monitoring the system somehow to generate the
tialize the pointer variable to nil before calling the function. event. The result is that these notifications can be a bit unreli-
Do not allocate an actual TTwoPIDLArray record. When the able, and often, there is a noticeable delay between the event and
function returns, this pointer will point to a TTwoPIDLArray the notification. Also, the shell has only a 10-item event buffer,

28 March 1999 Delphi Informant


Undocumented

var SHCNE_MEDIAINSERTED and


PIDLs: PTwoPIDLArray; SHCNE_MEDIAREMOVED aren’t generated in response
EventId: DWORD; to inserting or removing standard floppy diskettes. The disk
Lock: THandle;
begin drive hardware apparently doesn’t support this information.
// If NT, use the memory map to access the PIDL data.
if (SysUtils.Win32Platform = VER_PLATFORM_WIN32_NT) then If you’re deleting files into a Recycle Bin, you won’t get an
begin
Lock := SHChangeNotification_Lock(THandle( SHCNE_DELETE method, as you might expect. You’ll actu-
TheMessage.wParam), DWORD(TheMessage.lParam), ally get a SHCNE_RENAMEITEM. The
PIDLs, EventId); SHCNE_DELETE comes only after you empty the Recycle
if (Lock <> 0) then
try Bin. This makes sense if you think about it, because you’re
ProcessEvent(EventId, PIDLs); actually moving the file from its old path to the Recycle Bin’s
finally path, but it might not be completely intuitive at first.
SHChangeNotification_Unlock(Lock);
end;
end Some events can fire multiple times. This seems to apply to
else most file events. For example, if you delete a file, you’ll probably
// If this isn't NT, access the PIDL data directly.
begin get notified twice with identical messages. Be prepared for that.
EventId := DWORD(TheMessage.lParam);
PIDLs := PTwoPIDLArray(TheMessage.wParam); If you want to know more, it’s probably best that you test the
ProcessEvent(EventId , PIDLs);
end; items your particular application will be using, on as many plat-
end; forms as possible.
Figure 3: Example of using SHChangeNotification_Lock.
The Delphi Way
So much for the messy details demanded by the Windows API.
and may decide to consolidate a number of events with a generic Let’s design a component that will hide all this minutiae from
SHCNE_UPDATEDIR in case of an overflow. those Delphi developers who have better things to worry about.

In short, don’t depend on these notifications for mission- First, we’ll define the component’s public interface. There are
critical applications. two main issues with this component: where to watch, and
what to watch for. The API provides the capability to filter a
Don’t Believe Everything You Read few criteria: event type, interrupt or non-interrupt, the folder
Another problem is that the Windows documentation isn’t to watch for events, and whether that folder’s sub-folders
always completely accurate in its descriptions of the various should also be watched. Naturally, we also need some means
events. Following are variations from the documentation of turning this thing on or off.
we’ve observed after actual implementation.
Identifying the folders to watch is the only unusual aspect of
An SHCNE_ATTRIBUTES is supposed to happen when “the all this. As we mentioned previously, not all folders can be
attributes of an item or folder have changed.” However, we have identified by file system paths. Using PIDLs to identify these
only witnessed an SHCNE_ATTRIBUTES event occurring folders is impractical for the design-time property editor, how-
when the printer status changed. Changing file and folder ever, because PIDLs are opaque data types and can’t be manu-
attributes produces an SHCNE_UPDATEITEM event instead. ally edited by a developer.

SHCNE_NETSHARE and SHCNE_NETUNSHARE are sup- The solution is to use two properties. One property is a new
posed to occur when you share or unshare a folder. However, on enumerated type, TkbSpecialLocation, which encapsulates the list
Windows NT, the SHCNE_NETUNSHARE event never of Windows API constants that correspond to various “special”
occurs. You get a SHCNE_NETSHARE event on both occa- folders, e.g. Control Panel. These constants can be used with the
sions. On Windows 95, they appear to work as advertised. SHGetSpecialFolderLocation API function to obtain a PIDL to
that folder. By setting a property to one of these enumerated val-
An SHCNE_UPDATEIMAGE event is claimed to signify that ues, special locations can be selected without requiring the devel-
an image in the system image list has changed. However, images oper to type in the data for the PIDL. One of the values of
in the system image list should never change. What the event TkbSpecialLocation is kbslPath. Setting this value will enable a
really means is that something that was using that particular second property to allow the developer to enter a specific file sys-
icon index in the system image list is now using something else. tem path to monitor. Here’s the final list of published properties:
Typical uses of SHCNE_UPDATEIMAGE include the Recycle
Bin changing between empty and full, and the icon associated property Active: Boolean
with a CD drive when a CD is inserted or removed. Document property HandledEvents: TkbShellNotifyEventTypes
property InterruptOptions: TkbInterruptOptions
icons, which change as a result of changing a file-type associa- property RootFolder: TkbSpecialLocation
tion, do not generate an SHCNE_UPDATEIMAGE. They will property RootPath: TFileName
produce an SHCNE_ASSOCCHANGED event instead. property WatchChildren: Boolean;

29 March 1999 Delphi Informant


Undocumented

TkbShellNotifySimpleEvent = procedure(Sender: TObject;


4) Two non-nil PIDLs, which might represent paths.
IsInterrupt: Boolean) of object; 5) “Generic” events that have two raw PIDLs, either of
which might be nil.
TkbShellNotifyIndexEvent = procedure(Sender: TObject;
Index: LongInt; IsInterrupt: Boolean) of object;
The definitions of the procedural types representing these five
TkbShellNotifyGeneralEvent = procedure(Sender: TObject; event categories are shown in Figure 4. You may notice that
PIDL: Pointer; Path: TFileName;
IsInterrupt: Boolean) of object;
PIDL parameters are represented as Pointer types, rather than
PItemIDList types. This is because PItemIDList is defined in
TkbShellNotifyRenameEvent = procedure(Sender: TObject; the ShlObj unit, which is not added automatically to form
OldPIDL: Pointer; OldPath: TFileName; NewPIDL: Pointer;
NewPath: TFileName; IsInterrupt: Boolean) of object;
units when the component is dropped. This has the annoying
quality of causing compiler errors when an event is assigned,
TkbShellNotifyGenericEvent = procedure(Sender: TObject; unless unit ShlObj is manually added to the form’s interface
EventType: TkbShellNotifyEventType; PIDL1: Pointer;
PIDL2: Pointer; IsInterrupt: Boolean) of object;
part uses clause.

Figure 4: Event procedure type definitions. Deciding what the events will be was fairly simple. There
should be a component event for each possible shell event.
In addition, we’ll define events to encapsulate the three
property OnAnyEvent: TkbShellNotifyGenericEvent
property OnDiskEvent: TkbShellNotifyGenericEvent
“collective” events defined by the Windows API (for
property OnGlobalEvent: TkbShellNotifyGenericEvent those hardy souls who like working with raw Windows
property OnAssociationChanged: TkbShellNotifySimpleEvent data). The list of events and their definitions are found
property OnAttributesChanged: TkbShellNotifyGeneralEvent
property OnDriveAdded: TkbShellNotifyGeneralEvent
in Figure 5.
property OnDriveRemoved: TkbShellNotifyGeneralEvent
property OnExtendedEvent: TkbShellNotifyGenericEvent The last area of the public interface to consider, the run-
property OnFolderCreated: TkbShellNotifyGeneralEvent
property OnFolderDeleted: TkbShellNotifyGeneralEvent
time methods, lends a few candidates for consideration. It’s
property OnFolderRenamed: TkbShellNotifyRenameEvent handy to have auxiliary methods for setting the Active prop-
property OnFolderUpdated: TkbShellNotifyGeneralEvent erty on and off. Also, virtually any component that handles
property OnFreespaceChanged: TkbShellNotifyGeneralEvent
property OnImageUpdated: TkbShellNotifyIndexEvent
system data needs some sort of reset capability. These meth-
property OnItemCreated: TkbShellNotifyGeneralEvent ods are shown here:
property OnItemDeleted: TkbShellNotifyGeneralEvent
property OnItemRenamed: TkbShellNotifyRenameEvent
procedure Activate;
property OnItemUpdated: TkbShellNotifyGeneralEvent
procedure Deactivate;
property OnMediaInserted: TkbShellNotifyGeneralEvent
procedure Reset;
property OnMediaRemoved: TkbShellNotifyGeneralEvent
property OnNetworkDriveAdded: TkbShellNotifyGeneralEvent
property OnResourceShared: TkbShellNotifyGeneralEvent
property OnResourceUnshared: TkbShellNotifyGeneralEvent There are, of course, many details to implementing a com-
property OnServerDisconnected: TkbShellNotifyGeneralEvent ponent that go beyond the core functions that the compo-
nent encapsulates. As there are many other excellent refer-
Figure 5: Design-time published events.
ences that cover the details of implementing custom com-
ponents in Delphi, this article will not cover them. Instead,
For those developers who like to get down and dirty, we’ll sur- it will concentrate only on those pieces of the component’s
face a couple of run-time properties that allow them to med- implementation that directly relate to the specific problem
dle at the API level. There are really only two bits of informa- of shell notifications.
tion we can provide: the notification handle, and the actual
root PIDL. These properties are: The implementation of shell notifications revolves around the
calls to SHChangeNotifyRegister and SHChangeNotifyDeregister.
property Handle: THandle Everything we do in this component will be in support of
property RootPIDL: PItemIDList those function calls. Let’s outline how the public properties
and methods relate to those functions.
Because this component encapsulates a notification mech-
anism, it should be clear that events are at its heart. We’ll The property most directly linked to the calls is Active.
want to pre-crack the event data for the developer’s conve- Setting this property to True will cause
nience, of course. Every event will include the Sender para- SHChangeNotifyRegister to be invoked with parameters
meter, as usual, and a Boolean value identifying whether governed by the other four published properties. As you might
the event was generated by an event. Some review of the guess, setting it to False will cause SHChangeNotifyDeregister
API documentation reveals that we have five basic patterns to terminate the notifications.
of “data” parameters:
1) No extra parameters. The mechanics of calling the API functions are delegated to
2) One DWORD. two private methods, StartWatching and StopWatching, which
3) One non-nil PIDL, which might represent a path. will be discussed later. Meanwhile, here’s a code snippet from

30 March 1999 Delphi Informant


Undocumented
the private property writer method, SetActive, which illus- procedure TkbShellNotify.HandleMessage(
trates the logic at work: var TheMessage: TMessage);
var
// Do nothing if the new value is the same as the old. PIDLs: PTwoPIDLArray;
if (NewValue <> Self.FActive) then EventId: DWORD;
{ If we're activating, start watching. } Lock: THandle;
if (NewValue) then begin
Self.StartWatching; { Handle only the WM_SHELLNOTIFY message. }
else { If we're deactivating, stop watching. } if (TheMessage.Msg = WM_SHELLNOTIFY) then
Self.StopWatching; begin
{ If this is NT, use the memory map to access the
PIDL data. }
The other four properties (besides Active) can substantially if SysUtils.Win32Platform=VER_PLATFORM_WIN32_NT then
change the notification model, if they are modified. There begin
is no way to update these “on the fly” when Active is True, Lock := SHChangeNotification_Lock(THandle(
TheMessage.wParam), DWORD(TheMessage.lParam),
so it’s necessary to “reset” the shell notification if these PIDLs, EventId);
properties are changed while Active is True. This is the if (Lock <> 0) then
purpose of the Reset method. It simply calls the private try
Self.ProcessEvent(EventId, PIDLs);
methods StopWatching and StartWatching, if the compo- finally
nent is Active. This has the effect of stopping the notifica- SHChangeNotification_Unlock(Lock);
tions with SHChangeNotifyDeregister, and calling end;
end
SHChangeNotifyRegister with the current values of the { If this is not NT, access the PIDL data directly. }
component’s properties. The property writer methods for else
these four properties call the Reset method after updating begin
EventId := DWORD(TheMessage.lParam);
the component’s internal data member corresponding to PIDLs := PTwoPIDLArray(TheMessage.wParam);
that property. Self.ProcessEvent(EventID, PIDLs);
end;
end { if }
A Window All Our Own { Call the default Windows procedure for any other message. }
Next, we consider how to manage the shell notification mes- else
TheMessage.Result := DefWindowProc(Self.FMessageWindow,
sages, which will result from the call to SHChangeNotifyRegister. TheMessage.Msg,TheMessage.wParam,TheMessage.lParam);
A window must be available to process all the notification end;
messages generated by the shell. How best to provide this win-
Figure 6: An example message-handling method.
dow? We could use the application’s main form, but that
would require hooking that form’s window procedure, a messy
undertaking at best. It seems simplest to generate our own As we discussed earlier, we first check the message identifier to
invisible window, whose sole purpose is to handle those notifi- verify that this incoming message is a WM_SHELLNOTIFY
cation messages, and over whose destiny we have absolute message. We ignore all others, and send them to default han-
control. This step eliminates problems with conflicting mes- dling, because none of the miscellaneous messages typically
sages and clashing hooks. broadcast to every window in the system interest us.
WM_SHELLNOTIFY, if you were wondering, is a constant
The Delphi VCL thoughtfully provides a couple of functions we define ourselves, not one provided by Windows. Setting it
to facilitate this scheme. They are AllocateHWnd and equal to WM_USER is the easiest thing to do, and perfectly
DeallocateHWnd, found in the Forms unit. AllocateHWnd ’s safe because we use this window only for handling shell notifi-
entire purpose in life is to generate a handle to an invisible cation messages.
window, using a window message-handling procedure you
provide. Exactly what we need! Now, in the component’s Once we’re satisfied this is indeed a notification message,
constructor, we can get and store a handle to a window that we decode the LParam and WParam values to determine
has nothing better to do than manage our icon’s messages. which event the message is relating to us, and the associat-
Here’s a call to this handy method: ed PIDL data. Notice how we extract the data using the
SHChangeNotification_Lock API call if the component is
{ Allocate a message-handling window. } running on NT. You’ll also see that the actual detailed
Self.FMessageWindow := AllocateHWnd(Self.HandleMessage); handling of the message is delegated to another private
method, ProcessEvent, to avoid repeating that rather sub-
As you can see, the call is trivial. What’s important is the stantial bit of code.
message-handling procedure we passed. This is where the
notification messages from the shell are received, and where The private method ProcessEvent (see Figure 7) is where we
we have the opportunity to dispatch them. AllocateHWnd crack the PIDLs out of the TTwoPIDLArray record, convert
takes a single argument of TWndMethod, a class-member pro- them to file-system paths if possible, and dispatch them to
cedure that takes a single argument of type TMessage. It’s up the appropriate event handler. The centerpiece of this
to us to provide that procedure. Figure 6 shows our compo- method is a case statement that calls the event-handling
nent’s message-handling procedure, to give you the idea. method corresponding to the event type. These event-

31 March 1999 Delphi Informant


Undocumented

procedure TkbShellNotify.ProcessEvent(EventID: DWORD;


PIDLs: PTwoPIDLArray); procedure TkbShellNotify.StartWatching;
var var
EventType: TkbShellNotifyEventType; NotifyPathData: TNotifyRegister;
PIDL1: PItemIDList; Flags: DWORD;
PIDL2: PItemIDList; EventType: TkbShellNotifyEventType;
Path1: TFileName; EventMask: DWORD;
Path2: TFileName; begin
IsInterrupt: Boolean; { Initialize Flags. }
begin Flags := SHCNF_NO_PROXY;
{ Crack open the Two-PIDL array. } if kbioAcceptInterrupts in Self.InterruptOptions then
PIDL1 := PIDLs.PIDL1; Flags := Flags or SHCNF_ACCEPT_INTERRUPTS;
PIDL2 := PIDLs.PIDL2; if kbioAcceptNonInterrupts in Self.InterruptOptions then
{ Try to convert PIDLs to Paths. } Flags := Flags or SHCNF_ACCEPT_NON_INTERRUPTS;
Path1 := GetPathFromPIDL(PIDL1); { Initialize EventMask. }
Path2 := GetPathFromPIDL(PIDL2); EventMask := 0;
{ Determine if event is interrupt-caused. } for EventType := Low(TkbShellNotifyEventType) to
IsInterrupt := Boolean(EventID and SHCNE_INTERRUPT); High(TkbShellNotifyEventType) do
{ Iterate through possible events and fire as appropriate. if (EventType in Self.HandledEvents) then
This is necessary because event IDs are flags, and EventMask :=
there may be more than one in a particular message. } EventMask or ShellNotifyEnumToConst(EventType);
for EventType := Low(TkbShellNotifyEventType) to { Initialize Notification Path data. }
High(TkbShellNotifyEventType) do begin NotifyPathData.pidlPath := Self.RootPIDL;
{ Skip the "multi" event types.
NotifyPathData.bWatchSubtree := Self.WatchChildren;
They will be fired as needed below. }
{ Register for notification and store the handle. }
if (EventType in [kbsnAnyEvent, kbsnDiskEvent,
Self.FHandle := SHChangeNotifyRegister(
kbsnGlobalEvent]) then
Self.FMessageWindow, Flags, EventMask, WM_SHELLNOTIFY,
Continue;
1, NotifyPathData);
{ If the current event type is flagged... }
{ If registration failed, set Active to False. }
if ((ShellNotifyEnumToConst(EventType) and
if (Self.Handle = 0) then
EventID) <> 0) then begin
Self.Deactivate;
{ Fire appropriate "multi" events for this event. }
end;
Self.AnyEvent(EventType, PIDL1, PIDL2, IsInterrupt);
if ((ShellNotifyEnumToConst(kbsnGlobalEvent) and
ShellNotifyEnumToConst(EventType)) <> 0) then
Figure 8: The StartWatching private method.
Self.GlobalEvent(EventType, PIDL1,
PIDL2, IsInterrupt);
if ((ShellNotifyEnumToConst(kbsnDiskEvent) and
ShellNotifyEnumToConst(EventType)) <> 0) then The Heart of the Matter
Self.DiskEvent(EventType,PIDL1,PIDL2,IsInterrupt);
Now, with all these supporting tasks worked out, we can
{ Fire specific event. }
case (EventType) of finally get to the heart of the matter — the long-awaited
kbsnAssociationChanged: call to SHChangeNotifyRegister. It might seem a bit anticli-
Self.AssociationChanged(IsInterrupt);
mactic, but this function is found in only one place
{ Other event-handling methods with
appropriate parameters... } throughout the entire component. This place, of course, is
kbsnServerDisconnected: the StartWatching private method. Let’s work our way
Self.ServerDisconnected(PIDL1,Path1,IsInterrupt);
through it, as shown in Figure 8.
end; { case }
end; { if }
end; { for } First, we initialize the flags. We’ll always specify
end;
SHCNF_NO_PROXY, because we’re prepared to handle
Figure 7: An abbreviated event-processing method. the message correctly on NT. We’ll also set the
SHCNF_ACCEPT_INTERRUPTS and
SHCNF_ACCEPT_NON_INTERRUPTS flags, as deter-
handling methods are necessary because events are often mined by the value of the InterruptOptions property.
left unassigned by the developer, and calling these nil han-
dlers directly would cause exception faults. There is one Next, we initialize the EventMask parameter to specify the
such method for each published event that accepts the raw shell events we’re interested in monitoring. This is accom-
PIDLs and paths, performs any additional processing that plished by iterating through all possible values of the
may be required to extract useful information, and calls TkbShellNotifyEventType enumerated type, and setting the
the event handler if it has been assigned. corresponding flag bits using a logical or operation each time
we find a value, which is set in the HandledEvents property.
The other wrinkle is the for loop, which iterates through the
possible event types, comparing each to the EventID parame- The last parameter to set up is the folder path information.
ter using a logical and comparison to see if the matching We simply fill the necessary TNotifyRegister record with the
event handler should be fired. This is necessary because the values found in the RootPIDL and WatchChildren properties.
EventID parameter is actually a bitmap of flags, and, as such,
might include more than one event flag. This prohibits a sim- Finally, we can make the call to SHChangeNotifyRegister and
ple equality comparison using the = operator. save the handle returned by that function.

32 March 1999 Delphi Informant


Undocumented
The StopWatching method is quite simple by comparison. It
merely calls the SHChangeNotifyDeregister function, giving it
the handle saved from the call to SHChangeNotifyRegister,
and resets the internal data member containing the handle
value to zero. Here’s the entire method:

procedure TkbShellNotify.StopWatching;
begin
{ Deregister the notification handle
and set the handle to nil. }
SHChangeNotifyDeregister(Self.FHandle);
Self.FHandle := 0;
end;

The result is a component that makes shell notifications almost


trivial to access. Go ahead. Try it out with the sample event
monitor provided with this article’s companion source code.
Feel free to forget all that Windows API complexity.

The Bottom Line


The Shell Notifications API gives you powerful insight
into the internal workings of the Win32 shell. Your appli-
cations can use it to react to all sorts of important system
events, giving you that extra edge over the competition.
The TkbShellNotify component gives you this API in a
convenient and simple package, making these services
almost trivial to exploit. Now, get out there and write
something amazing! ∆

The files referenced in this article are available on the Delphi


Informant Works CD located in INFORM\99\MAR\DI9903KB.

Kevin J. Bluck is an independent contractor specializing in Delphi development.


He lives in Sacramento, CA with his lovely wife Natasha. He spends his spare
time chasing weather balloons and rockets as a member of JP Aerospace
(http://www.jpaerospace.com), a group striving to be the first amateur organiza-
tion to send a rocket into space. Kevin can be reached via e-mail at
kbluck@ix.netcom.com.

James Holderness is a software developer specializing in C/C++ Windows


applications. He also runs a Web site on undocumented functions in Windows
95 (http://www.geocities.com/SiliconValley/4942). He is currently working
for FerretSoft LLC (http://www.ferretsoft.com), where he helps create the
Ferret line of Internet search tools. James can be reached via e-mail at
james@ferretsoft.com or jholderness@geocities.com.

33 March 1999 Delphi Informant


New & Used

By Bill Todd

ReportBuilder Pro 4.0


Newcomer Is Worth Switching To

A lthough ReportBuilder is a relative newcomer to the family of Delphi report-


ing tools, it’s been worth the wait. If you’re looking for a single reporting
tool with different layouts for different pages, side-by-side bands, newspaper-style
columns, end-user reporting, and fast drag-and-drop report layout, this is it. (This
review is based on a pre-release version of ReportBuilder 4.0, so there may be
some differences between the features described here and the shipping product.)

To build a report, begin by dropping a database source. You can also create your
TTable and TDataSource on a Delphi form. own pipeline components to supply data
Next, move to the ReportBuilder page of from proprietary sources. Set the pipeline
the Component palette, and drop a component’s DataSource property to con-
TppBDEPipeline and TppReport on your nect it to your DataSource component,
form. The BDEPipeline component is one and you’re ready to double-click the Report
of a family of pipelines that supply data component to open the designer, shown
to your report. Use BDEPipeline to get in Figure 1.
data from a BDE (Borland Database
Engine) database, or the DBPipeline if Ergonomically Speaking
you’re working with a BDE replacement. The designer is well laid out and easy to
The TextPipeline component lets you use use. The first row of toolbars includes the
data in a text file for your report, and the non-data-aware, data-aware, and advanced
JITPipeline supplies data from a non- components. The next row begins with a
toolbar (which is empty in Figure 1). The
contents of this toolbar vary depending on
the component you select in the designer.
For example, if you select a data-aware
component, the toolbar contains a drop-
down list of data pipeline components, and
a drop-down list of fields from the current-
ly selected pipeline. To the right is a text-
control toolbar that will be familiar to any-
one who has used a Windows word proces-
sor. At the end of the formatting toolbar
are Bring To Front and Send To Back but-
tons for working with layered components.

The third row of toolbars begins with one


that is unique: the nudge bar. The four but-
tons on this toolbar let you nudge the select-
ed components one pixel up, down, left, or
right. You can use a keyboard combination of
C plus the appropriate arrow key to achieve
Figure 1: The ReportBuilder report designer. the same effect. Either method makes precise

34 March 1999 Delphi Informant


New & Used
alignment easy. However, the two components on the Advanced toolbar —
The next set of Region and SubReport — really set ReportBuilder apart. The
toolbars pro- Region component allows you to group components, includ-
vides a full set ing subreports, so they will be positioned as a group, both at
of sizing and design time and run time.
alignment
options, The SubReport component is a complete report, including
including bands, that you can position within another report. The
Figure 2: The Position dialog box. grow-to-largest power of subreports is limited only by your imagination. For
and shrink-to- example, you can use subreports to create a report that con-
smallest, in both the horizontal and vertical directions; align sists of several sections, each of which has its own layout and
left, right, or center; space the selected components evenly data source. When you add subreports to a report, a tab is
either horizontally or vertically; and center the selected com- added to the bottom of the report designer for each subre-
ponents horizontally or vertically within their band. port. Simply click on a tab to go to the design view for that
subreport. You can also place subreports in regions, and place
You can also position and size components with great preci- them next to each other within bands of the main or other
sion by right-clicking and displaying the Position dialog box subreports. This means you could print a report that shows
(see Figure 2). The Position dialog box lets you specify size information about a customer, displays a list of that cus-
and position to three decimal places. The designer makes tomer’s locations, and displays a list of the products that cus-
extensive use of context menus, so if you want to do some- tomer has ordered. Subreports also give you the ultimate in
thing to a component, simply right-click on it; the pop-up power for creating multi-level master-detail reports.
menu will likely offer the choice you need.
ReportBuilder gives you total control of relative positioning
From the View | Toolbars menu, you can open the Report when you place multiple components that can stretch, such
Tree and Data Tree windows, shown in Figure 3. The Report as subreports or memos, in the same band. Suppose you want
Tree allows you to see all the components in each band of to print a memo, and below the memo, you need to print
your report, and to select one or more components. This is some other fields from a database. Simply set to True the
handy for working with layered components. The Data Tree ShiftWithParent property of the components below the
shows all the fields for each pipeline component on the memo, and they will move down as the memo component
report. You can drag fields from the Data Tree and drop them expands. Suppose you also want to put a frame around the
in any band of your report. When you drop a field, the memo component in your report. Simply drop a Shape com-
appropriate data-aware control is created automatically. You ponent on top of the memo, set its StretchWithParent proper-
can also choose whether dropping a field creates a data-aware ty to True, and send it to the back. Now the Shape compo-
control, a label containing the field name, or both. nent will automatically expand and contract to fit the size of
the memo in each record as it’s printed. If you have multiple
Suite Components stretching components positioned vertically in the same
ReportBuilder has a full suite of data-aware and non-data- band, you can set each component’s ShiftRelativeTo property
aware components to print any type of data on your report. to ensure they print in the correct order relative to each other.

Number Crunching
ReportBuilder allows you to create calculated
values in several ways. First, of course, you
can add Delphi calculated fields to your
datasets. Second, the TppDBCalc component
lets you calculate the sum, count, min, max,
or average for a group of records. Finally, the
TppVariable component lets you add code to
its OnCalc event handler to generate values
any way you wish. If the value of one
TppVariable component uses the value of
another, you can set its CalcOrder to ensure
the variables calculate in the correct order to
produce a correct result. ReportBuilder also
lets you choose to generate reports in one
pass or two. One pass is faster, but using a
two-pass report lets you display grand totals
at the beginning of the report, or use “page n
Figure 3: The Report Tree and Data Tree windows. of m” page numbers.

35 March 1999 Delphi Informant


New & Used
Ease of Use ReportBuilder ships with an
ReportBuilder lets you give your end users the ability to excellent user manual in the
design and save reports with unparalleled ease and control. form of a Word document.
If you can safely turn your users loose in your database, a The foundation of the manual
query wizard helps them create SQL statements to fetch is a series of tutorials of gradu-
ReportBuilder 4.0 is a relative new-
the data on which to report. If the structure of the data- ally increasing complexity that comer to the family of Delphi report-
base is too complex (or the tables too large) to let an teach you how to use all the ing tools, but if you’re looking for a
single reporting tool with different lay-
untrained user create queries and reports, you can create power of the product with a outs for different pages, side-by-side
data views for the user to work with. A data view is a class minimum investment of time. bands, newspaper-style columns, end-
user reporting, and fast drag-and-drop
that provides a level of abstraction between the database The tutorials cover everything report layout, this is it. I’ve looked at
and the user. You create data views that contain query from creating master-detail many reporting tools, and none of
them compare with ReportBuilder 4.0.
components that return a subset of data from one or more and master-detail-detail
tables. End users employ the data views to create reports reports, to putting reports in Digital Metaphors Corp.
16775 Addison Road, Suite 613
without direct access to the database. DLLs and using the Dallas, TX 75248
JITPipeline component to
Phone: (972) 931-1941
The report designer for end users is identical to the print a report from a string E-Mail: info@digital-metaphors.com
designer used by developers. One feature of the designer grid. The manual also contains Web Site: http://www.
digital-metaphors.com
particularly useful for end-user reporting is the ability to an excellent introduction to Price: ReportBuilder, US$249;
design a report and save it as a template. Templates can be the report designer, as well as ReportBuilder Pro, US$495; upgrade
from ReportBuilder to ReportBuilder
saved to individual disk files, or to a BLOb field in a data- an explanation of all its tools Pro, US$246.
base. By creating and saving a template, you can give your and time-saving features, where
end users a starting point from which they can more easily to find them, and how to use
create the reports they need. ReportBuilder also includes a them. If you invest a few hours in working through the
data dictionary component that allows you to create more manual and tutorials, you’ll find that you are instantly pro-
readable aliases for table and field names. If you build a ductive when you start building reports in your applications.
data dictionary, end users will only see the more meaning-
ful alias names. ReportBuilder 4.0 is available in two versions. The Pro ver-
sion includes the end-user reporting capability. Both versions
ReportBuilder 4.0 has its own programming language, support Delphi 1 through 4, and include full source and a
RAP (Report Application Pascal). You and your end users 30-day, money-back guarantee.
can use RAP to write event handlers for any object in a
report. This lets you easily create the code for calculated Conclusion
values in the report, and change the properties of any I shudder at the thought of changing reporting tools. I will
object as the report runs. For example, you could change still have to support all my old applications using the old tool
the color of a field depending on whether the value is pos- for a long time, and I will have to endure the pain of becom-
itive or negative. RAP is great for developers because it ing proficient with a new tool. But ReportBuilder is worth
means you create and deploy complete reports, including the trouble. The ability to produce any kind of report my
event handlers, without having to change your executable. clients need, as well as provide end-user reporting using a sin-
For end users, RAP provides the means to build very gle tool, will make life so much easier in the future that the
sophisticated reports. When you implement end-user change is worth the effort. I’ve looked at a lot of reporting
reporting, ReportBuilder lets you set the level of RAP tools, and none compare with ReportBuilder 4.0. ∆
access available to your end users so you can match the
complexity to their needs.

The Bottom Line Bill Todd is president of The Database Group, Inc., a database consulting and
I hate creating reports. One of the first things I look for development firm based near Phoenix. He is a Contributing Editor of Delphi
in a reporting tool is how fast it lets me create the dozens Informant, co-author of four database-programming books, author of over 60
of simple reports that most applications require. The icing articles, and a member of Team Borland, providing technical support on the
on the cake is the Report Wizard. While it’s a great tool Inprise Internet newsgroups. He is a frequent speaker at Inprise conferences in
for end users, it saves time for developers as well. After the US and Europe. Bill is also a nationally known trainer and has taught
setting up your data sources and pipeline components, Paradox and Delphi programming classes across the country and overseas. He
fire up the Report Wizard. It will let you choose a report was an instructor on the 1995, 1996, and 1997 Borland/Softbite Delphi World
Tours. He can be reached at bill@dbginc.com or (602) 802-0178.
style, select the fields to be displayed, and define the
groups you require; click the Finish button to create your
report layout. Even if the report requires additional manu-
al customization, getting the basic layout created without
having to manually place and align each component is a
real time saver.

36 March 1999 Delphi Informant


File | New
Directions / Commentary

The Multimedia APIs

U nlike TAPI and some of the newer APIs, the multimedia APIs have been a part of Delphi since its
inception. Unfortunately, the documentation and examples (in Delphi and elsewhere) are minimal.
Increasingly, I get messages from developers struggling to work with these APIs and frustrated by the lack
of information, so I’m writing a book on the topic (The Tomes of Delphi: 32-bit Multimedia Programming
will be published by Wordware this summer). I will also be exploring selected multimedia topics in these
pages. This column, which presents an overview of the multimedia APIs, is the first such contribution.

The 32-bit multimedia APIs fall into three general groups — low- uct. The command strings consist of words arranged in English-like
level, mid-level, and high-level — with low-level APIs more device- sentences. For example, the words associated with the playback of a
specific, high-level APIs more generic, and mid-level APIs some- sound include “load,” “play,” and “stop.” For example:
where in between. In Windows 3.x, the MCI was the highest-level
API available. With Windows 95, a higher-level interface was intro- mciSendString("pause movie", nil, 0, nil);
duced: the MCIWnd class.
Conveniently, these command strings have corresponding command
Low-level APIs. The low-level functions require more work, but messages, but messages are more complicated. The command mes-
provide a high degree of control over various media devices. These sage corresponding to the above command string looks like this:
APIs include functions to work with various multimedia file types,
audio, audio mixers, and joysticks. They also include the most pre- mciSendCommand(MovieDevice, MCI_PAUSE, 0, DWORD(nil));
cise timing functions in the Windows API. Considering multime-
dia’s time-critical requirements, this should come as no surprise. As where MovieDevice is the handle of the movie-playing device
Bob Swart demonstrated at last summer’s Inprise Conference, these returned by calling another command message using the
timing functions can even be used to profile applications. MCI_OPEN command. As you can see, there’s quite a bit more
involved with command messages than with command strings.
The Waveform API provides a low-level interface to audio devices. Sometimes, you’re required to supply the third and fourth parame-
Most of these functions begin with “Wave,” or more specifically, ters (flags and parameters, respectively). In a running application,
“WaveIn” or “WaveOut.” The former group provides functions for command messages are much faster than command strings, because
recording .WAV files, the latter for playback. Because this is a low- the latter must be parsed.
level API, you generally need to call several of these functions. Often
grouped with these files, but at a higher level, the PlaySound function A high-level class. The MCIWnd class is even easier to use, provid-
provides an easier means of playing .WAV files. Closely related to ing a quick way to create a multimedia control and associate it with
these functions are the auxiliary audio functions that control auxiliary some type of multimedia event. For example, the following state-
audio devices. All of these are prefixed with “Aux,” e.g. AuxSetVolumn. ment (based on code from Delphi’s online help) creates a button on
a form that, when pressed, plays a video clip:
Another important set of low-level functions are those that control the
MIDI (Musical Instrument Device Interface), enabling communica- MCIWndCreate(hwndParent, g_hinst, WS_VISIBLE or WS_CHILD or
tion with a sound card’s built-in synthesizer. With these MIDI func- MCIWNDF_SHOWALL, 'sample.avi');
tions, you can work with sounds directly, select various patches
(instruments), and perform many sound-playing operations on the fly. Because this column is introductory in nature, it probably raises
more questions than answers. However, I plan to explore these top-
The low-level multimedia input/output functions provide a variety of ics in more detail in future articles. I also plan to devote another
file operations with buffered and unbuffered files, files in standard column to sources of information on multimedia programming,
Resource Interchange File Format (RIFF), memory files, or files using both books and Internet sites. In the meantime, I’d like to hear
custom formats. Similarly, there are low-level functions for working from you concerning your experiences, questions, and discoveries in
with .AVI files. These include a large number of routines for file I/O, working with multimedia in Delphi. ∆
streaming AVI data, and editing AVI streams. Finally, there are several
functions for working with joysticks, and a handful of timer functions. — Alan C. Moore, Ph.D.

The MCI. The Media Control Interface is a mid-level API that pro- Alan Moore is a Professor of Music at Kentucky State University, special-
vides a fairly easy means of working with a variety of multimedia izing in music composition and music theory. He has been developing
files and devices, so you can create a sophisticated multimedia appli- education-related applications with the Borland languages for more than
cation with minimum coding. Best of all, the MCI provides two 10 years. He has published a number of articles in various technical jour-
approaches (two sets of commands) for performing these tasks. The nals. Using Delphi, he specializes in writing custom components and
first, mciSendString, is ideal for prototyping applications; the second, implementing multimedia capabilities in applications, particularly sound
mciSendCommand, is faster and better suited for the finished prod- and music. You can reach Alan on the Internet at acmdoc@aol.com.

37 March 1999 Delphi Informant

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy