On The Cover: November 1999, Volume 5, Number 11
On The Cover: November 1999, Volume 5, Number 11
On The Cover: November 1999, Volume 5, Number 11
ON THE COVER
6 Greater Delphi
CORBA Dennis P. Butler
FEATURES
16 Undocumented
RTTI Gets Easier Bill Todd
Although it remains undocumented, a new set of functions added to
TYPINFO.PAS in Delphi 5 make Delphis Run-time Type Information
capabilities easier to use as Mr Todd explains.
REVIEWS
35 Raize Components 2.1
Product Review by Ron Loewy
20 DBNavigator
The Data Module Designer Cary Jensen, Ph.D.
Delphi 5 is full of new features; one of the more important for database
developers is its Data Module Designer. Dr Jensen provides us with an
introduction and a detailed description of the new tool.
25 Sound + Vision
Extending TAPI Robert Keith Elias and Alan C. Moore, Ph.D.
Mr Elias and Dr Moore review TAPI concepts, overview the TAPI and
Multimedia API, and explain the code needed to play .WAV files to, and
record them from, a phone line and much more.
1
DEPARTMENTS
2 Delphi Tools
5 Newsline
38 File | New by Alan C. Moore, Ph.D.
Delphi
T O O L S
New Products
and Solutions
environment.
e-TEST Suite 3.1 also features
Visual Script extensibility and
enhanced support for JavaScript
and VBScript.
RSW Software, Inc.
Price: From US$4,995
Phone: (508) 435-8000
Web Site: http://www.rswsoftware.com
Delphi
T O O L S
New Products
and Solutions
Delphi
T O O L S
New Products
and Solutions
to GIF images.
With Help & Manuals ability to merge multiple help files
and convert cross-references
among separate parts into
intra-file links, different people
can work on different parts of
the help file development.
While designed as a development tool for programmers,
Help & Manual can be used
by network administrators,
business people, and anyone
who wants to create printed
manuals or structured HTML
documents with hyperlinks.
EC Software
Price: US$229 for a single-user license;
additional licenses cost US$49 each.
Phone: +43 6212-7838
Web Site: http://www.ec-software.com
News
L
November 1999
Greater Delphi
CORBA / Delphi 4, 5 / VisiBroker / JBuilder
By Dennis P. Butler
CORBA
Creating Clients and Servers
VisiBroker CORBA
VisiBroker is the ORB (Object Request Broker)
used throughout this article and in the examples.
VisiBroker is the Inprise implementation of the
CORBA standard that adds many additional features to assist developers when creating applica-
Greater Delphi
Internet Client
Web Server
VisiBroker
for Java ORB
VisiBroker
for Java ORB
Java Applet
VisiBroker
for C++ ORB
Naming Services
C++ Objects
Gatekeeper
Smart Agent
Internet
VisiBroker
for C++ ORB
C++
Application
Enterprise Client
Intranet/
Enterprise
Firewall
Intranet Client
VisiBroker
for Java ORB
Java Object
Event Service
Smart Agent
Java
Application
VisiBroker
for Java ORB
To create the server and its object, start a new Delphi application
and save the form and project. You may want to shrink the dimensions of the form, as this will be your server application running on
your machine. For this example, I have named the files Cserver.dpr
and Cmain.pas. From the main menu of Delphi, select File | New,
then select the CORBA Object item on the Multitier page. The
CORBA Object Wizard will be displayed (see Figure 2).
As you can see, the object to be defined is named OnlineAuction, will
be a shared instance, and will be single-threaded. The information
required by this dialog is described in more detail below.
Class Name. Enter the base name of the object that implements the
CORBA interface for your object. Filling in the class name will do two
things; it will create a class of this name with a T prepended, and create an interface for the class using this name with an I prepended.
Instancing. Use the Instancing combo box to indicate how your
CORBA server application creates instances of the CORBA object.
There are two possible values:
Instance-per-client A new CORBA object instance is created
for each client connection. The instance persists as long as the
connection is open. When the client connection closes, the
instance is freed.
Shared instance A single instance of the CORBA object handles all client requests. Because the single instance is shared by
all clients, it must be stateless.
Greater Delphi
change their values. As well see, the Type Library editor
takes care of this as well.
The Type Library editor is also used to define interfaces
to COM objects; in fact, this was the original purpose of
the Type Library editor. Because of this, it has some features that arent used for CORBA objects. An example of
this is the Help information shown in Figure 3. Also,
some of the data types that are available in the Type
Library editor may not be CORBA-compliant data
types. Developers can easily research CORBA data types
through the Delphi or VisiBroker help files.
Figure 4: Completed
type library tree.
Greater Delphi
Once the refresh button has been clicked, the Type Library editor
can be closed, and the source file can be saved (CSrvObj.pas in this
case). We now have our server object interface defined, and the
Pascal code shell from which we can add functionality for the
object. The Type Library editor has created some files for us, such as
the _TLB stub file thats created based on the server application project name. In this example, since the project was named CServer, its
CServer_TLB.pas. Since this file is automatically generated, no additional work is needed on it. This file sets up stub and skeleton classes for the server object, as well as defining several other classes that
may be used, such as the CORBA object factory class and the COM
CoClass class. The only thing we really need to know at this point is
that the CORBA shell has been created for us from how we defined
the interface in the Type Library editor, and a TLB file has been created from which we can get our object reference for the server.
Our server source file csrvobj.pas has been filled in from the Type
Library editor with the methods and properties that were defined.
The empty shell, csrvobj.pas, is shown in Listing One. Now we
need to code the implementation of the object. We need to add a
few private variables to hold the current high-bid price, the current
high-bid customer, and the product being bid on. We also need to
initialize these private variables in the constructor for the object.
Finally, we need to implement the object with code to provide functionality to the methods that have been created for us. The completed source, with comments, is shown in Listing Two.
All that was provided by Delphi was the code shell; the rest had to
be filled in to give the object interface an implementation. We now
have the server for our object. To use this server of our CORBA
object, all we need to do is add the CSrvPas unit to the uses clause
of any form of a project. When this is done, the initialization code
for the object will be fired when that form is used. Thus, the server
will be started, and an object will be created that is available for use.
Greater Delphi
CorbaObj to the uses section to perform the binding necessary to
communicate across the ORB.
Figure 5 contains an image of our client. It has functionality to
refresh the bid information, enter information for a new bid, and
place the bid.
from the command line, or run VisiBroker Smart Agent from the
VisiBroker folder installed in the Delphi folder. The -C at the command line designates that the osagent will run in the taskbar; otherwise it will not appear there, so it may not be apparent whether its
running while youre testing. Once the ORB Smart Agent is running, start the server application.
Once the server has been started, its a good idea to ensure that the server objects are available for any clients. The VisiBroker utility, osfind, can
be used to do this. Run osfind from the command line on the client
machine to display a list of available objects within the subnet of the
machine. This will verify that the client has access to the necessary server objects. For complicated implementations of CORBA, the osagents
can be configured to look for server objects, or other osagents outside
the current subnet. Although this isnt covered in this article, suffice it to
say there are facilities to allow the client to look virtually anywhere for a
server object, as long as the osagents have been set up correctly.
The final step is to run several clients. These should automatically
get a reference to the server by including the type library file that
was generated, and the client should have access to all server functions. In our example, we can launch many clients from different
machines (within the same network subnet), and make successive
bids against the server.
The repository name can be anything you want, and will launch the
Interface Repository application. Once its open, you can either select
File Load from the main menu, and select the IDL file that was exported above, or run the following statement from the command line:
idl2ir <IDL File Name>
Once this has been done, weve registered our interface with the
Interface Repository. To verify that the interface has been registered
with the Interface Repository, click the Lookup button after running the above line from the command line or loading the IDL
directly. You should see something similar to Figure 7.
The only step left at this point is to create the client that will access the
Interface Repository, and use an object stored there. To start with, well
use the same form layout as in the early binding example. Start a new
project in Delphi. Instead of naming it Client.dpr (as in the early binding example), name it Client_DII.dpr. Copy the client form from the
early binding example, and save it as cclient_dii.pas. Also, change the
Caption to designate that its the DII version of the form. Finally, rename
the form itself to TfrmDynamicCorbaClient. These changes arent necessary, but serve to distinguish the different client implementations.
Greater Delphi
our server object. Tools such as JBuilder and C++Builder
can be used to provide additional clients or servers based on
this IDL file. In this example, well use JBuilder because its
a wholesale departure from the Delphi/C++Builder IDE.
In JBuilder, create a new application with a single frame.
In the project manager, add the CServer.idl file we saved
above. The file will appear in your JBuilder projects file
list. Right-click on the IDL file and select Build. This
runs the IDL file through the IDL2JAVA precompiler.
The IDL2JAVA precompiler converts the IDL file to
Java stub classes. The generated Java files can be used to
create CORBA servers to implement these objects or
CORBA clients to access the objects.
Design the frame so that it looks similar to the Delphi
client that was designed earlier. Figure 9 shows what was
done for our Java client.
The code for the early binding Java client will be similar
to the Delphi client; well have variables for the object
factory, and a server object that will be obtained from
that factory. The code for the client is shown in Listing Five.
Figure 8: Desktop with server, two static clients, and one dynamic client.
Since we arent going to use the stub that was generated for us,
remove the reference to Cserver_TLB in the uses clause of the client
that was carried over from the early-binding example. The code is
slightly different since we dont have the client stub to give us a
direct reference to the interface. We use the TAny class, a CORBA
interface type for DII, to get a reference from the interface repository
for our server object. In this case, well get an instance of the object
factory, which will get a reference to the server object. This is done
to mimic the process of the non-DII client shown earlier.
Aside from additional coding needed to get references to our server
object through the object factory, the code for the late binding client
remains largely identical to the early binding client. The code for the
second client is shown in Listing Four. The server, and the early and
late binding clients running at the same time, is shown in Figure 8.
To review, here are the steps that were taken to run the server and
two types of clients on the same machine:
Start the ORB Smart Agent (osagent -C)
Start the Server (CServer.exe)
Run the Interface Repository (irep inprisepso)
Load the interface into the Interface Repository
(idl2ir CServer.idl)
Run the early binding client (Client.exe)
Run the late binding client (Client_DII.exe)
As you can see, weve declared the object factory and interface in our
source file. In the constructor for the frame, theres a different method
performed here than in Delphi to attach to the ORB and obtain an
object reference. All that needs to be known is that the Java application is getting a reference to the server object through the use of automatically-generated Helper files. By doing this, an object reference is
obtained, and is used in the same manner as the Delphi client. Helper
files and other CORBA files are generated from the IDL2JAVA utility,
which was run when the CServer.idl file was compiled. JBuilder uses
this method to create the stub and skeleton files, as compared to using
the Type Library editor in Delphi.
Once an object reference has been obtained, the code for the frame
itself is similar to the Delphi application. The Delphi CORBA server
has no knowledge of what language is being used for requests; Delphi
and Java clients make virtually identical calls to the server object
Greater Delphi
through their stub files. Our Java client could have been running on a
UNIX machine located on a different continent from our Delphi
server. As long as the CORBA subnet or osagents were configured
correctly, these separate processes could talk to each other just as easily
as if they were on the same machine. This simple Java/Delphi example provides a mere glimpse of the full potential of CORBA.
Conclusion
Theres no doubt that CORBA will continue to gain momentum in
enterprise computing due to its tremendous assets: flexibility, language independence, and a wide range of capabilities for virtually
every distributed need. Delphi combines these assets with RAD
development to make CORBA programming easier and faster for
the developer, without sacrificing CORBAs capabilities. As we saw
in these simple examples, Delphi is an ideal platform for setting up
CORBA clients and servers for many types of applications.
Inprise developers also have advanced CORBA capabilities available
through the use of the MIDAS technology. MIDAS allows users to create complicated queries easily through Delphi, and pass query results
back from remote datasets using CORBA as the transportation format.
This technology is especially powerful, because developers dont need to
create complicated objects to hold query output; MIDAS automates
this task, creating stub and skeleton classes automatically. The MIDAS
technology is available in several Inprise development tools and will
continue to play a key part in RAD CORBA development.
Going forward, Delphi developers can expect to see more CORBA
support in new releases of Delphi. The IDL2PAS utility, when
released, will give Delphi developers access to all CORBA features
and will not limit implementations to the framework that Delphi
has provided. This will provide the best of both worlds: RAD development for standard CORBA tasks as covered in this article, and
granular CORBA development for more specific and complicated
implementations through IDL2PAS.
Delphi has long been regarded as the best Windows development
tool. With the merging of CORBA technology to Delphi, this reputation will only grow as Delphis capabilities now reach across previously unbreakable boundaries, such as multiple operating systems
and languages.
The files referenced in this article are available on the Delphi Informant
Works CD located in INFORM\99\NOV \DI9911DB.
IOnlineAuction)
protected
function Get_ProductName: WideString; safecall;
function GetCurrentPrice: Double; safecall;
function GetCurrentUser: WideString; safecall;
function PlaceBid(Amount: Double;
const CustomerName: WideString): Integer; safecall;
procedure Set_ProductName(const Value: WideString);
safecall;
end;
implementation
uses
CorbInit;
function TOnlineAuction.Get_ProductName: WideString;
begin
end;
function TOnlineAuction.GetCurrentPrice: Double;
begin
end;
function TOnlineAuction.GetCurrentUser: WideString;
begin
end;
function TOnlineAuction.PlaceBid(Amount: Double;
const CustomerName: WideString): Integer;
begin
end;
procedure TOnlineAuction.Set_ProductName(
const Value: WideString);
begin
end;
initialization
TCorbaObjectFactory.Create('OnlineAuctionFactory',
'OnlineAuction', 'IDL:CServer/OnlineAuctionFactory:1.0',
IOnlineAuction, TOnlineAuction, iSingleInstance,
tmSingleThread);
end.
Dennis P. Butler is a Senior Consultant for Inprise Corp., based out of the Professional
Services Organization office in Marlboro, MA. He has presented numerous talks at
Inprise Developer Conferences in both the US and Canada, and has written a variety
of articles for various technical magazines, including CBuilderMag.com. He can be
reached at [email protected], or (508) 481-1400.
12
Greater Delphi
AFactory: TCorbaFactory); override;
protected
// Accessor methods for FProductName property.
function Get_ProductName: WideString; safecall;
procedure Set_ProductName(const Value: WideString);
safecall;
// Function to get the current price for the
// auction product.
function GetCurrentPrice: Double; safecall;
// Function to get the current customer for the
// auction product.
function GetCurrentUser: WideString; safecall;
// Function to place a new bid.
function PlaceBid(Amount: Double;
const CustomerName: WideString): Integer; safecall;
end;
implementation
interface
// Included with Delphi to initialize CORBA object.
uses
CorbInit;
// Overridden create for our object.
constructor TOnlineAuction.Create(Controller: IObject;
AFactory: TCorbaFactory);
begin
inherited;
// Initialize our private variables.
FProductName := '<NA>';
FCurrentCustomer := '<NA>';
FCurrentPrice := 0;
end;
// Method to get the property value for the current
// auction product.
function TOnlineAuction.Get_ProductName: WideString;
begin
Result := FProductName;
end;
// Method to set the property value for the current
// auction product.
procedure TOnlineAuction.Set_ProductName(
const Value: WideString);
begin
FProductName := Value;
end;
// Method to get the current price of the high bid.
function TOnlineAuction.GetCurrentPrice: Double;
begin
Result := FCurrentPrice;
end;
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, CorbaObj, CServer_TLB, StdCtrls, Mask,
Buttons;
type
TfrmStaticCorbaClient = class(TForm)
lblCurrentProduct: TLabel;
lblBidPrice: TLabel;
lblProduct: TLabel;
lblCurrentHighBidPrice: TLabel;
lblPrice: TLabel;
btnRefresh: TBitBtn;
edtBidPrice: TEdit;
lblCustomerName: TLabel;
edtUserName: TEdit;
btnMakeBid: TBitBtn;
lblCurrentHighBidUser: TLabel;
lblUser: TLabel;
lblBidStatus: TLabel;
lblPlaceNewBid: TLabel;
procedure FormCreate(Sender: TObject);
procedure btnRefreshClick(Sender: TObject);
procedure btnMakeBidClick(Sender: TObject);
public
// Our interface to the server object.
AuctionInterface : IOnlineAuction;
end;
var
frmStaticCorbaClient: TfrmStaticCorbaClient;
implementation
{$R *.DFM}
13
Greater Delphi
begin
// Update price and customer name information for
// current product.
lblPrice.Caption := FloatToStrF(
AuctionInterface.GetCurrentPrice, ffCurrency, 18, 2);
lblUser.Caption := AuctionInterface.GetCurrentUser;
lblProduct.Caption := AuctionInterface.Get_ProductName;
end;
// Call object to place a new bid against the server.
procedure TfrmStaticCorbaClient.btnMakeBidClick(
Sender: TObject);
begin
// Do some client-side data checking to save speed.
if edtUserName.Text = '' then
begin
ShowMessage('You must enter a user name first.');
Exit;
end;
// Validate floating point value.
try
StrToFloat(edtBidPrice.Text);
except
ShowMessage('Invalid amount entered.');
end;
// Place Bid.
case AuctionInterface.PlaceBid(
StrToFloat(edtBidPrice.Text), edtUserName.Text) of
0 : ShowMessage('Bid amount insufficient.');
1 : ShowMessage('Bid successful!');
end;
// Refresh information.
btnRefresh.Click;
end;
end.
14
implementation
{ $R *.DFM }
procedure TfrmDynamicCorbaClient.FormCreate(
Sender: TObject);
begin
try
// Bind to ORB instance for object factory.
AuctionFactory :=
ORB.Bind('IDL:CServer/OnlineAuctionFactory:1.0');
// Create reference to server object from factory.
AuctionServer := AuctionFactory.CreateInstance('');
except
ShowMessage('Failed to connect to server.');
raise;
end;
// Refresh information on screen.
btnRefresh.Click;
end;
procedure TfrmDynamicCorbaClient.btnRefreshClick(
Sender: TObject);
begin
// Update price and customer name information for
// current product.
lblPrice.Caption := FloatToStrF(
AuctionServer.GetCurrentPrice, ffCurrency, 18, 2);
lblUser.Caption := AuctionServer.GetCurrentUser;
lblProduct.Caption := AuctionServer.Get_ProductName;
end;
procedure TfrmDynamicCorbaClient.btnMakeBidClick(
Sender: TObject);
var
rlBidPrice : Double;
sBidUser : WideString;
begin
// Do some client-side data checking to save speed.
if edtUserName.Text = '' then
begin
ShowMessage('You must enter a user name first.');
Exit;
end;
// Validate floating point value.
try
StrToFloat(edtBidPrice.Text);
except
ShowMessage('Invalid amount entered.');
end;
// Place Bid; use local variables as intermediaries
// to calls.
sBidUser := edtUserName.Text;
rlBidPrice := StrToFloat(edtBidPrice.Text);
case AuctionServer.PlaceBid(rlBidPrice, sBidUser) of
0 : ShowMessage('Bid amount insufficient.');
1 : ShowMessage('Bid successful!');
end;
// Refresh information.
btnRefresh.Click;
end;
end.
Greater Delphi
import
import
import
import
jButton1_mouseClicked(e);
}
});
jLabel5.setForeground(Color.blue);
jLabel6.setText("Auction User Name");
jLabel7.setText("Bid Price");
jButton2.setText("Place Bid");
jButton2.setText("Place Bid");
jButton2.addMouseListener(
new java.awt.event.MouseAdapter() {
public void mouseClicked(MouseEvent e) {
jButton2_mouseClicked(e);
}
});
jLabel5.setText("Place New Bid");
jlblCurrentBid.setText("< NA >");
jLabel4.setText("Current Bid Status");
jLabel3.setText("Current High Bid User Name");
jLabel2.setText("Current Product");
this.setLayout(xYLayout1);
this.add(jLabel1, new XYConstraints(57, 50, -1, -1));
this.add(jLabel2, new XYConstraints(93, 31, -1, -1));
this.add(jLabel3, new XYConstraints(21, 68, -1, -1));
this.add(jLabel4, new XYConstraints(6, 3, -1, -1));
this.add(jlblCurrentBid,
new XYConstraints(207, 51, 183, -1));
this.add(jlblCurrentProduct,
new XYConstraints(207, 31, 182, -1));
this.add(jlblCurrentUser,
new XYConstraints(207, 70, 176, -1));
this.add(jButton1,
new XYConstraints(138, 100, 102, 30));
this.add(jLabel5, new XYConstraints(8, 146, -1, -1));
this.add(jLabel6, new XYConstraints(72, 182, -1, -1));
this.add(jLabel7, new XYConstraints(130, 206, -1, -1));
this.add(jtfUserName,
new XYConstraints(188, 182, 145, -1));
this.add(jtfBidPrice,
new XYConstraints(188, 205, 85, -1));
this.add(jButton2,
new XYConstraints(139, 230, 105, 35));
com.sun.java.swing.*;
borland.jbcl.layout.*;
java.awt.event.*;
borland.jbcl.control.*;
}
// Place new bid button.
void jButton2_mouseClicked(MouseEvent e) {
Double rlTotal = new Double(jtfBidPrice.getText());
if (pOnlineAuction.PlaceBid(
rlTotal.doubleValue(),
jtfUserName.getText())==0) {
Message m = new Message(this, "Sorry",
"Bid Amount Insufficient");
m.show();
}
else {
Message m2 = new Message(this, "Success",
"Bid Successful");
m2.show();
}
}
catch(org.omg.CORBA.SystemException e) {
System.err.println("System Exception");
System.err.println(e);
}
try {
jbInit();
}
catch (Exception e) {
e.printStackTrace();
}
}
// Refresh button.
void jButton1_mouseClicked(MouseEvent e) {
Double rlTotal =
new Double(pOnlineAuction.GetCurrentPrice());
// Update price and customer name information
// for current product.
jlblCurrentBid.setText(rlTotal.toString());
jlblCurrentUser.setText(
pOnlineAuction.GetCurrentUser());
jlblCurrentProduct.setText(
pOnlineAuction.Get_ProductName());
}
}
private void jbInit() throws Exception {
xYLayout1.setHeight(311);
xYLayout1.setWidth(400);
jLabel1.setText("Current High Bid Price");
jLabel4.setForeground(Color.blue);
jlblCurrentProduct.setText("< NA >");
jlblCurrentUser.setText("< NA >");
jButton1.setText("Refresh");
jButton1.addMouseListener(
new java.awt.event.MouseAdapter() {
public void mouseClicked(MouseEvent e) {
15
Undocumented
RTTI / Delphi 5
By Bill Todd
un-time Type Information (RTTI) is the information the compiler stores about the
published properties of objects in your application. And it is very useful stuff. For
example, RTTI is the mechanism the Object Inspector uses to determine the properties
an object instance has, their data types, and current values.
RTTI has remained a mystery to many Delphi
programmers for two reasons:
RTTI has never been documented. The only
information on RTTI is the source code for the
TYPINFO.PAS unit, much of which is in
assembler; this is the only reference in Delphi 5.
The RTTI functions make extensive use of
pointers to Pascal records and arrays of
records. The Pascal language hides the use of
pointers almost completely, so many
Pascal/Delphi programmers arent comfortable
working with them.
A new set of functions was added to
TYPINFO.PAS in Delphi 5 to make RTTI easier to use. This article will explore the new
easy RTTI functions, and ways in which you
can use them.
The single most useful thing about RTTI is that
it gives you access to an object instances properties without having to know anything about
the object. For example, suppose you want to
iterate through all the components on a form
and change their color. Your first inclination
might be to try this:
16
for I := 0 to Pred(Form1.ComponentCount) do
Form1.Components[I].Color := clRed;
Undocumented
Function
Description
IsPublishedProp
GetPropInfo
IsStoredProp
PropIsType
PropType
GetObjectPropClass
TTypeKind(I);
This ability to freely convert between strings and enumerated types means you can use enumerated types internally
in your program to make it faster and smaller, allow the
use of case statements, and convert the enumeration
members to strings for output.
The remaining new RTTI functions exist in pairs consisting of a method to get and a method to set the value of
each of the following property types:
Ordinal
Enumerated
Set
Object
String
Float
Variant
Method
Int64
Figure 3: The edit box contains the value of the VisibleButtons property.
Following this comment are the prototypes for the new RTTI
functions added in Delphi 5. These functions can be separated
into two categories. The first group provides information about the
published members of an object instance. The second group of
functions gets and sets the values of properties of varying types.
Figure 2 lists the information functions.
The types used by PropIsType and PropType are not standard
Pascal data types. PropIsType takes three parameters. The first is
the object instance to interrogate, the second is the name of the
property, and the third is a value of type TTypeKind, which specifies the type of property you are testing. PropType takes two parameters, an object reference and the name of a property, and
returns a value of type TTypeKind. TTypeKind is an enumerated
type declared in TYPINFO.PAS as:
type
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration,
tkFloat, tkString, tkSet, tkClass, tkMethod, tkWChar,
tkLString, tkWString, tkVariant, tkArray, tkRecord,
tkInterface, tkInt64, tkDynArray);
and TKind will be set to tkString. If you need the name of the enumerated type as a string, another RTTI function, GetEnumName,
will do the job. After calling:
S := GetEnumName(TypeInfo(TTypeKind), Ord(TKind));
17
The syntax of all these functions is the same. The get functions
take two parameters, an object instance and a property name, and
return the value of the property. The set procedures take three
parameters. The first is the object instance, the second is the
property name, and the third is the value to assign to the property. There are also two functions, GetPropValue and SetPropValue,
that work with properties of any type compatible with a variant.
GetPropValue returns the property value as a variant and
SetPropValue takes a variant as its third parameter, which supplies
the value to be assigned to the property.
The use of most of these functions is intuitive, but there are some
exceptions. First, although there are many Boolean properties in
the VCL, there are no functions to get and set Boolean properties.
You can handle Boolean properties using GetVariantProp and
SetVariantProp, as shown in Figure 1. Another alternative is to use
GetOrdinalProp and SetOrdinalProp and use 0 to represent False or
-1 to represent True.
GetSetProp and SetSetProp are also a bit different. When you call
GetSetProp to return the value of a set property, the returned value is
a string. Figure 3 shows a form from the sample application that displays the value returned for the VisibleButtons property of a
DBNavigator component. Note that the value shown in the edit box
is a comma-separated list of the members of the set. The GetSetProp
function also takes a third parameter named Brackets. If Brackets is
Undocumented
procedure TPropTypeForm.CallTestBtnClick(Sender: TObject);
var
TheMethod: TMethod;
begin
TheMethod := GetMethodProp(TestBtn, 'OnClick');
if Assigned(TheMethod.Code) then
TNotifyEvent(TheMethod)(Sender)
else
ShowMessage('No event handler.');
end
There are two members of this record you may find useful. The first
is Name, which contains the name of the property, and the second is
PropType. PropType is of type PPTypeInfo and is declared as:
PPTypeInfo = ^PTypeInfo;
PTypeInfo = ^TTypeInfo;
TTypeInfo = record
Kind: TTypeKind;
Name: ShortString;
end;
Undocumented
works because the memory for the array was allocated
by calling AllocMem. AllocMem not only allocates the
requested block of memory, but also initializes each
byte to nil.
The call to PropertyList.Items.Add adds the name of the
property and the property type to the listbox. The name
is referenced by Props^[I].Name. When working with
pointers, the trailing caret can be read as points to, so
this notation identifies the name field of element I of the
array to which Props points.
The reference to the name of the propertys type,
Props^[I].PropType^.Name, can be read as the Name
field in the record pointed to by the PropType field in
element I of the array pointed to by Props. (For more
information about using pointers, see the Delphi
Language Reference.)
Figure 6: The List Properties form.
Figure 6 shows the List Properties form from the sample application with the properties of the listbox displayed in the left listbox and the events in the right
listbox. The code for the List Events button is identical to the
code in Figure 5 except that tkMethod is used for the second
parameter in the call to GetPropList.
Conclusion
RTTI is a vital tool if you want to write reusable code. Using
RTTI its easy to write a procedure that takes a data module as
its parameter and will set the ReadOnly property of all of the
datasets in the data module to True. You can also write a security
system that allows different rights to be defined for different
components on a form or data module. When your application
creates a form, it can call a routine that takes the form as a parameter, looks up the users rights for the components on that
form, and sets the components ReadOnly or Visible property
based on the users rights. The uses of RTTI are endless.
The files referenced in this article are available on the Delphi Informant
Works CD located in INFORM\99\NOV \DI9911BT.
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, a co-author of four database-programming books, an author of over
60 articles, and a member of Team Borland, providing technical support on the
Borland Internet newsgroups. He is a frequent speaker at Borland developer conferences in the US and Europe. Bill is also a nationally known trainer and has
taught 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 [email protected], or (602) 802-0178.
DBNavigator
Delphi 5 / Database Design
Until recently, such component sharing was the primary reason for using data modules. With the
release of Delphi 5, however, theyre far more valuable. This is because Delphi 5 provides you with
the Data Module Designer. In addition to the sharing already described, the Data Module Designer
adds three new capabilities: two different visual representations of the relationships between your components, partial automation of property definitions,
and the ability to comment and print your data
module design for documentation purposes.
Overview
The Data Module Designer, shown in Figure 1, is
a replacement for the form-like designer displayed
20
DBNavigator
Using the Tree view to configure properties
isnt limited solely to dropping new components. Existing components can be
dragged from one node to another, thereby
causing their properties to be updated. For
example, imagine that you have two database components on your data module:
one whose DatabaseName is IBSERVER
and which connects to an InterBase server,
and another whose DatabaseName is
LOCALDATA and which is used to point
to a local directory. If you now place a new
Query component into the LOCALDATA
node, but meant to place it into the
IBSERVER node, you can simply drag the
query from LOCALDATA to IBSERVER,
which causes the DatabaseName property
of the query to change from the old value
to that of the new node. Likewise, the
SessionName property will also be updated,
but only if the two database components
use different sessions (which is unlikely).
Figure 2: The expanded Fields node for a query.
Data set nodes in the Tree view also have subnodes. Nodes for Table
components have subnodes for FieldDef, TField, and IndexDef definitions. ClientDataSet nodes include all Table nodes plus nodes for
Aggregates and Params. Nodes for Query and StoredProc components have TField and Params nodes. All data sets have a
Constraints node.
When the appropriate property of a specific data set is defined, the
corresponding node can be expanded. The expanded property node
provides you with direct access to the associated object within the
Object Inspector. For example, in Figure 2, the Fields node of the
query named CustSalesQuery has been expanded. The expanded
node provides you with access to each TField object created for this
query using the Fields Editor. If you select one of these TField
objects, its properties are displayed in the Object Inspector. Figure 3
shows the Object Inspector when the EmpFullName field has been
selected. This field is a lookup field.
You can also right-click a data sets subnodes in the Tree view to display a limited popup menu. This menu permits you to do basic
things with the selected node, such as add an item. Or, if the rightclicked item is a leaf, such as a specific Constraint, the menu allows
you to easily remove the item.
The glyphs used by the nodes in the Tree view can convey information about the associated object. For example, a component whose
definition is incomplete is enclosed by a red circle. This is shown in
Figure 4, which is how a Tree view looks after a single Table has
been dropped onto it, but before any properties, such as
DatabaseName or TableName have been set.
In some cases, a glyph appears grayed out. Although IDE users often
associate this state with a disabled object, in the Tree view it means that
a default object is being used. For example, if youre using the default
session, the session nodes name will be Default, and its glyph will
appear grayed out. Youll see this effect for both the session and the
database (Alias) in Figure 4. However, if you refer back to Figure 1 or 2,
youll see that only the default session is used, in which case the glyph
for the session is grayed out, but the glyph for the database is not.
DBNavigator
However, components dropped into the Tree view
will be positioned in the center of the Component
Tree (the same position as components that are
placed when you double-click in the Component
palette), so the most common use of the Components
page is to reorganize the position of components,
providing a more ordered display for components
dropped into the Tree view.
Figure 5: A Data Diagram containing a variety of data access components and comments.
22
DBNavigator
the simple diagram shown in Figure 6. This diagram
contains only two relationships, one between a data
source, and one between two queries (master-detail).
(Note as well the labels that name the relationships
being depicted.)
There are two types of changes you can make that will
affect the reference lines that appear. The first is that
you can move a component to a new location. At initial design, you will do this repeatedly, placing components in relative proximity to the components they
reference. Moving a component produces an automatic re-draw of its lines.
The second change involves manipulating the connecting lines directly. By clicking your mouse somewhere
on a reference line, you automatically add a new point
to that line. That point can then be dragged to a new
location. Although this creates a more complicated
polyline (a line with multiple points), you can drag
two or more of these points so the resulting line
doesnt obscure or intercept other lines.
DBNavigator
want to adopt a consistent and meaningful pattern for identifying objects. For example, you may want to make all lookup
tables use one color of header. To adjust the visual characteristics
of an object, right-click it and select from its displayed popup
menu. Data access components, reference lines, and labels can all
be adjusted in this same manner.
A Documentation Asset
Figure 9: The Print Tree View dialog box.
The last two buttons, the Comment Block and Comment Allude
tools, are used to create comments and then point them to one
or more objects on the data diagram. To place a comment block,
select the Comment Block tool, then drag a rectangle in the data
diagram. Next, click the comment block until a cursor appears.
After typing your comment, click on any other object to complete your comment. Pressing E while entering a comment
deletes any changes.
If your comment block text describes a specific object, youll generally want to add a comment allude to connect the comment with
the object(s) it references. To place a comment allude, click the
Comment Allude tool, then drag a line from the comment block to
the object you want to allude. Repeat this process if you want to
allude the comment block to additional objects.
The new Data Module Designer also provides you with a new
tool for documenting your applications. The Tree view and the
Data Diagram can both be printed. Printing the Tree view permits you to document some or all components on the data module. To print the Tree view, right-click in the Tree view area and
select Print. The Tree view responds by displaying the Print Tree
View dialog box shown in Figure 9. Notice that this dialog box
permits you to print either the entire Tree view, or only the select
component and its child nodes.
Printing the Data Diagram permits you to document the relationships youve defined visually (typically a subset of the components
appearing in the Tree view). To print the Data Diagram, rightclick anywhere within the Data Diagram and select Print from the
displayed popup menu. This displays the Print Data Model dialog
box, shown in Figure 10. Select how you want the diagram to be
printed and click OK.
One final note about the Data Diagram is in order. In most
cases, youll want to limit the size of your data diagram. Large
data diagrams can easily become congested, reducing their usefulness. For this reason, you might find it necessary to use more
data modules than in previous versions of Delphi, keeping each
data module relatively simple.
Conclusion
Delphi 5s Data Module Designer provides you with a wealth of
new features to make the configuration of your data access components more efficient. It also provides you with several new means of
documenting your data access components. Together, these capabilities of the Data Module Designer serve as one of many compelling
reasons to upgrade to Delphi 5.
The files referenced in this article are available on the Delphi Informant
Works CD located in INFORM\99\NOV \DI9911CJ.
Sound + Vision
TAPI / Wave API / Delphi 4, 5
Extending TAPI
Playing and Recording Sounds during Telephony Calls
TAPI Basics
As in the previous series of articles, well make
extensive use of the TAPI.pas conversion produced
by Project JEDI (http://www.delphi-jedi.org). Well
also use the Multimedia APIs (mainly the Wave
API) included in mmsystem.pas. One could easily
be intimidated by either of these large collections of
structures and functions. Fortunately, we can ignore
most of TAPIs 125 functions, and most of those in
mmsystem.pas, and still accomplish quite a lot.
However, we do need to know what were doing.
First we need to understand the difference
between lines, phones, calls, addresses, and IDs.
Lines. A line device generally refers to a modem or
a similar piece of hardware connected to a telephone line. TAPI doesnt communicate directly
with a line/modem. Instead it uses a TSP
(Telephone Service Provider). A TSP is just a fancy
25
name for a driver, written by the modem/equipment supplier, to communicate with TAPI.
Many TAPI functions are designed to talk to TSPs
that communicate with sophisticated telephone
equipment. Such TSPs are needed for large offices
where, for example, dozens of calls may arrive at
nearly the same time on a single line. People who
work with such systems must be able to manage
call conferencing and call transferring, among
other tasks. Unfortunately, the TAPI documentation rarely states the context within which a function is intended to operate. As well discuss later,
TAPI has certain limitations along with the functionality well be using here.
Phones. Another TAPI device is the phone. You
might assume that phone is synonymous with
handset. That would be a mistake. In TAPI, the
phone is the speaker and microphone attached to
your computer. You use phone devices to redirect
caller output to the speaker and to redirect input
from the user (via the microphone) to the telephone line. If youre not interested in using the
speakers, you can ignore those functions that take
the form phoneXxxxx.
Calls. The key event in the TAPI universe is the
call. It begins at the precise moment when
Windows sends a message to your application
telling you it has picked up the line. This is done
through a callback routine. Callback routines are
used throughout the Windows API to provide a
means for Windows to send information back to a
calling application. Our application uses three
callback routines: one for playing sounds, one for
recording sounds, and the one used by TAPI.
Sound + Vision
Most calls are initiated with the lineAnswer or lineMakeCall function. These and similar functions are asynchronous. This means that
when the function is called successfully, it immediately returns with
a positive number (1, 2, etc.). However, the function isnt really
complete until a confirming message is sent back via the callback
routine with the same positive number in the dwParam1 argument,
and zero in the dwParam2 argument.
Addresses and IDs. An address consists of a string of characters (letters, digits, and control characters) that provide a path to a phone or
other device. That other device could be a modem or a computer.
While addresses are often just phone numbers, they can also provide
a path to a network or Internet address.
IDs are simply handles to devices. In the case of TAPI, were usually
most interested in the handle to the logical line we get by calling the
lineGetID function. As complex as TAPI can be, at least we dont
have to work directly with the COM port.
Segment
RIFFxxxx
used in the specific .WAV file type are RIFF, fmt, fact, and data.
Other possible chunk types in multimedia files include cue and
playlist. Except for RIFF and data, these chunks can be in any order.
Why do we need to know this? If we want to record or play a
.WAV file using the low-level multimedia input/output functions, well need to correctly write or read the elements of the
header file. The xxxx parts simply indicate the size of their
respective chunks in numbers of bytes, making it possible to
write or read the precise number of bytes in our audio data.
Once weve correctly filled in the header chunks, we just feed
them to the multimedia input/output functions, and the data is
automatically written to, or read from, memory. Well explore
the details when we describe the code.
Lets take a closer look at some of the chunks, particularly the fmt
and fact chunks. The fact chunk can be calculated from the fmt
chunk, so well deal with that first. Figure 2 shows the name, type,
and use of each field in the fmt chunk.
If youve worked with electronic sound, youve probably encountered the word sample. A sample contains an elemental unit of
sound. The sample size is given in wBitsPerSample. For uncompressed (PCM) data, the size is usually 8, 16, 24, or 32 bits wide.
The wider the size, the higher quality the sound. Because were
using a telephone line, we can use low-quality resolution.
Therefore, well work with either 8 or 16 bits in wBitsPerSample.
When using a compression algorithm, this value is usually smaller.
Explanation
Data Type
Represents
Word
Word
nSamplesPerSec
DWord
nAvgBytesPerSec
DWord
nBlockAlign
Word
wBitsPerSample
Word
cbSize
Word
The fact chunk is optional in a simple PCM .WAV file. As
shown, every .WAV file of this type begins with a header
section that is typically about 100 bytes long. The chunks Figure 2: Elements of the fmt chunk.
26
Sound + Vision
The sample rate, which indicates how many samples are fed to the
speaker each second, is given in nSamplesPerSec. This is typically
described in Hertz (Hz); thus 8,000 Hz is 8,000 nSamplesPerSecond.
Common audio sampling rates are 8,000, 11,025, 22,050, and 44,100
Hz. Again, because analog telephone lines function at 3,100 Hz, nothing above 8,000 Hz will produce noticeably improved performance.
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\
MediaResources\acm
Having discussed the basic issues, were almost ready to delve into
the code. To test the code, youll need a modem that can handle
voice as well as data (most of the newer ones do), Unimodem/V, and
TAPI 1.4 or TAPI2.x at a minimum. Note that Unimodem/V comes
with Windows 98. You can perform the following test to determine
whether Unimodem/V has been successfully installed on your
machine. Look under Multimedia in Control Panel and ensure you
see the following two lines:
Voice Modem Wave #00 Line
Voice Modem Wave #00 Handset
You may also want to study Microsofts TAPI documentation, and
the WaveForm documentation. As we mentioned earlier, the TAPI
code for our application is based on code first published in Delphi
Informant in the summer of 1998. We took the liberty of making
certain changes to that code, and added quite a bit of new code.
First we changed the variable names to match those in the Microsoft
API documentation, which makes the code easier to follow. We
appended a record instance prefix rTAPI to these names to
make it easy to distinguish these variables from those used for playing sounds. The latter have a prefix of either rWvR (record Wave)
and rWvP (play Wave). See Glbvars.pas for the full declaration of
these structures (available for download; see end of article for
details). Weve also included many comments in the code.
We should point out that PCM is very costly in memory and disk
usage. In principle, its possible to convert PCM files to one of the
compressed formats; however, it would first be necessary to convert
the msacm.h header file into a Pascal format (another task for
Project JEDI, or an ambitious developer).
Weve included routines for dialing out and answering calls. Being able
to record conversations after dialing out may seem useless. However, it
eliminates the need for someone to call you to test the play and record
functions if you only have one telephone line. Because the modem cannot distinguish between digit tones generated by the local handset and
On the other hand, we were able to play (output to the modem) any
a remote handset, you could implement code to play or record mes.WAV file for which we had an installed CODEC, provided that
sages by pressing the keypad on either a remote or the local telephone.
However, the present version doesnt include
this functionality. Now lets investigate the
Field
PCM file
IMA ADPCM file
code for playing a sound.
fmt
18
20
wFormatTag
Wave_FORMAT_PCM (1) Wave_FORMAT_IMA_ADPCM (17) Using Wave API to Play a Sound
nChannels
1
1
The waveOutWrite function plays Wave
nSamplesPerSec 8000
8000
data to the phone line through the
modem. However, the task is hardly trivial.
nAvgBytesPerSec 16000
4055
Of course we could use the PlaySound or
nBlockAlign
2
256
sndPlaySound functions to play .WAV files,
wBitsPerSample 16
4
but they dont work in this context because
cbSize
0
2
they always play to the speaker by default.
fact
4
4
The waveOutWrite function provides the
dwFileSize
82294
82294
format translation and redirection capabiliActual file size
164,696 bytes
41,788 bytes
ties we need. Thus, the modem will receive
Figure 3: Comparison of PCM and IMA ADPCM files.
27
Sound + Vision
Nine steps are needed to play a .WAV file to a modem line. We have
numbered these steps in the code in the playWav.pas file (this file is
available for download; see end of article for details):
1) Get the COM port handle where output will be sent.
2) Open the .WAV file.
3) Locate the format information in the .WAV files header and
copy that information into a WAVEFORMATEX structure.
4) Prepare and lock a memory buffer for the Wave data.
5) Copy the Wave data into a memory buffer.
6) Prepare a WAVEHDR structure to describe the memory buffer
in which the Wave data is held.
7) Load the Wave conversion drivers and prepare a callback routine.
8) Play the .WAV file.
9) Clean up everything.
Lets discuss the details. The first step, getting the COM port handle, will be familiar to readers who followed the earlier series of articles. For this step, we use the TAPI function, lineGetID. If you want
to send a message to the line, use this function with a format like:
lineGetID(,,,,'wave/out');
The Wave API will use this handle (a number like 0, 1, 2, etc.) to
find the modem port to which Wave data will be sent. This information is sent in a predefined TAPI structure called a VARSTRING
via lineGetID (see Figure 4).
In the second step, open the .WAV file. This time well use the
mmioOpen function, one of the multimedia input/output functions.
This special-purpose function is especially useful for accessing multimedia data. With the file open, we need to locate the format information in the header. For this well use the mmioDescend function
twice, followed by GetMem. The latter allocates memory for the
rWvP.lpWaveFormat field based on the size of the chunk.
Then we deal with the data in the WAVE subchunk. We collect the
value in the DWORD xxxx, which immediately follows the
WAVEfmt marker. This gives us the size of the format section,
which begins right after this xxxx. We place an implicit pointer
here for the mmioRead operation in the next step. In that step we
copy the format information into a WAVEFORMATEX structure.
We use the mmioRead function to place the .WAV file format information into a predefined WAVEFORMATEX structure pointed to
by our variable, lpWaveFormat. The WAVEFORMATEX structure
is identical to the fmt structure just given. Now we can access the
bits per sample as lpWaveFormat^.wBitsPerSample.
You might be wondering if we could have just copied this information
straight from the header using regular Delphi streaming methods (or
block read and block write), rather than using the multimedia input/output functions described here. Of course we could, but that would involve
writing even more code. These multimedia functions are designed for
working with this kind of data and save us some additional work.
Next we prepare and lock a memory buffer for the Wave data. For
this operation we again use mmioDescend and GetMem. This time
we collect the value in the DWORD xxxx, which immediately follows the data marker. This is the size of the data section, which
constitutes most of the file. This data begins right after the xxxx,
28
where again an implicit pointer is placed for the mmioRead operation that we undertake in the next step.
Now we copy the Wave data into a memory buffer. We use mmioRead
to place the .WAV file data information into a memory buffer pointed
to by our variable, lpWaveData, which was returned by GetMem.
Because .WAV files for telephones are usually quite small, this may be
practical; however, for most operations, its common practice to use at
least two buffers (double buffering) so that waveOutWrite can play the
contents of one buffer while data is being loaded into the other.
At this point, we can close the .WAV file because everything we
need is already in memory. We prepare a WAVEHDR structure that
describes the memory buffer where Wave data is held. For this we
first use GetMem to provide a block of memory for the WAVEHDR
structure and a pointer to it that well name lpWaveHdr. A
WAVEHDR structure has the format shown in Figure 5.
We copy our pointer to the Wave data, lpWaveData, into our structure
at lpWaveHdr^.lpData, and the buffer length into
lpWaveHdr^.dwBufferLength. We use the information in this structure in
the waveOutPrepareHeader and waveOutWrite functions. Conveniently,
waveOutWrite also updates this structure as it plays the .WAV file.
In the next step, we need to load the Wave conversion drivers and
prepare the callback routine. The waveOutOpen function performs
these two steps. Most voice modems come with drivers to convert
.WAV files to voice signals. A few can even take the Wave output
directly without special drivers.
To check for this functionality, waveOutOpen is usually called once for
a test with the WAVE_FORMAT_QUERY flag set, and with the
WAVE_MAPPED flag not set. If waveOutOpen returns an error, its
called again with WAVE_MAPPED set. Otherwise, WAVE_MAPPED
isnt set. If WAVE_MAPPED is set, more resources are used and the
operation will be slower. The waveOutOpen function also defines the
address of a callback routine. Windows sends messages to this callback
routine when playing has started, stopped, or paused. There are three
possible messages the Wave API can send to this callback routine:
lineGetID(
// Get a physical device ID.
rTapi.hLine, // Handle to line from lineOpen.
0,
// Subaddress of rTapi.hLine.
// From lineMakeCall (rTapi.hCall replaced by 0 here).
myZeroHC,
// Use information from rTapi.hLine (not rTapi.hCall).
LINECALLSELECT_LINE,
// Pointer to VARSTRING structure where information
// is returned.
lpVarString(Port),
// "device class" (COM, Wave/out etc).
szDeviceClass);
Sound + Vision
1) WOM_OPEN is sent when waveOutOpen is called.
2) WOM_DONE is sent when waveOutWrite is finished playing
data, or waveOutReset is called.
3) WOM_CLOSE is sent when waveOutClose has completed a close.
Finally, were ready to play the .WAV file. To play a block of Wave
data, we first call waveOutPrepareHeader. If were swapping blocks to
save memory (double buffering), we must call this function for each
block before we call waveOutWrite. We use the waveOutWrite function to play the .WAV file to the phone line.
Finally, we need to clean things up. After our application receives
the WOM_DONE message, our code calls the various cleanup
functions, freeing resources. If an error has occurred, the file is
closed, waveOutUnprepareHeader is called, and allocated memory
resources are freed depending on the flags set when the finally clause
is reached in the try..finally block.
//
//
//
//
Sound + Vision
cnrRWaveHdr: WAVEHDR
lpData
:
dwBufferLength
:
dwBytesRecorded :
dwUser
:
dwFlags
:
dwLoops
:
lpNext
:
reserved
:
= (
nil;
0;
0;
0;
0;
0;
nil;
0);
//
//
//
//
//
//
//
//
//
Recording
Before recording, we must make final preparations. Once the
waveInPrepareHeader function has informed the Wave API about the
WAVEHDR structure, the waveInAddBuffer function must actually
set it up in preparation for playing. If were using double buffering, we
must prepare separate WAVEHDR structures for each memory buffer
and set up each with waveInPrepareHeader. When a particular buffer
has been filled, its associated dwFlags is set to WHDR_DONE so that
the buffer can be saved and then set up again.
The waveInPrepareHeader and waveInAddBuffer functions are always
used together; the former to prepare the data buffer headers, and the
latter to actually place the data buffer on the input queue to be
recorded. For large files, these data blocks can be re-used once the
application has recorded the material in them. However, a detailed
discussion of this process is beyond the scope of this article.
Finally, we are ready to begin recording from the line. The
waveInStart function begins recording sound data coming from the
phone line to a memory buffer. At this point, we must wait for a
WIM_DATA completion message. Once the memory buffer is full,
a WIM_DATA message is sent to the applications main callback
routine, the DefaultHandler. In our case, we use this message as a
trigger to call the ShutDownRecording function. If there are no
errors, the first thing it does is call the SaveMessageTooFile routine.
Now were ready to save the data to a file, a process of three steps.
First we create a file with the name msg#.wav. If we record more
than one message while the application is open, the first one is
named msg1.wav and the second msg2.wav. The resulting Waves can
easily be played simply by creating shortcuts, and then clicking on
them. If you rename one or the other to Greeting.wav, it can also be
played back over the telephone line.
Next, we update our custom Wave header record named
cnrRiffWaveHeader (the one with the RIFF, fmt, fact, and data
chucks), filling it with information returned to us from the
WAVEHDR structure in dwBytesRecorded. This quantity is essential to calculating the correct values for the Wave header.
Once these values have been calculated, the header is saved to the open
file. Finally, we write the Wave data to the file. Here again,
dwBytesRecorded comes in handy. We close the file and proceed to clean
everything up. The primary cleanup steps (apart from freeing memory)
consist of executing two functions, waveInUnprepareHeader and
waveInClose. You need to call waveInUnprepareHeader before you free
memory for its associated buffer. The waveInClose function simply closes
the Waveform input device and marks all pending buffers as done.
communicate with modems. The most generally annoying characteristic of TAPI is that on inexpensive standard modems (anything
under US$300), a calls progress cannot be monitored. Specifically,
you cant tell when someone has answered an outgoing call, or when
the line is hung up on the other end. It may be possible to do this
by recording line activity with Wave functions and examining the
noise level. However, we dont think that would be a trivial task.
Another problem thats commonly reported is that digit detection
seems to be disabled after a .WAV file finishes playing on certain
modems. The program we have created may not have this problem
because we reinitialize TAPI every time a call is completed (instead
of reusing the line handles and TAPI instances). While this might
eliminate certain problems, we consider it extremely inelegant. Also,
it might cause problems if were working with multiple lines or multiple applications sharing the same line.
A further limitation is that with standard modems, you cant collect
information from the line before a call is open. Therefore, there is
no obvious way of detecting that someone is dialing out on a line
connected to the modem unless a user opens a call first from within
the application. There is also no method for knowing if an incoming
call is a voice, fax, or data call before the call is picked up. In fact,
even determining this after the call has been picked up presents an
interesting challenge. Finally, because Unimodem/V isnt supported
on Windows NT below version 5.0 (Windows 2000), there is no
support for voice in that environment.
Conclusion
We are at the end of a rather long journey that has enabled us to add
sound playing and recording capabilities to our TAPI applications.
Along the way, we have learned more about the various Windows APIs
and how they work together. As far as exploring the Wave API, we have
only scratched the surface just enough to get the job done. In an
upcoming article, Dr Moore will explore this API in more detail, and
will explain other facets of it. He will also devote an upcoming File |
New column to multimedia and communications resources.
We used a number of online sources while preparing this article.
One such site contains much information on programming with
sound; visit http://www.wotsit.org, or the Usenet newsgroup at
news:comp.os.ms-windows.programmer.multimedia.
The files referenced in this article are available on the Delphi Informant
Works CD located in INFORM\99\NOV \DI9911RE.
By G. Bradley MacDonald
31
Description
FormatDSPPFM
(Display Physical
File Information)
DSPFD (Display File
Description)
Displays the contents of a file in a fixed column format. This is an easy, quick way to view
the data in a file. Format:
DSPPFM FILE(library/file)
Displays information about the file itself. It contains all the information about the file, such
as initial size, extents, etc. When used to view a table or view that was created using SQL
statements, it shows the full SQL statement used to create the file. It contains no field
information. Format:
DSPFD FILE(library/file)
DSPFFD (Display
File Field Description)
Displays all the field information, such as field name, field type, field size, start position,
end position, etc. Format:
Starts a debugging session on an OS/400 job. This can be either the current job or a service
job. While this command has parameters, when using it with a client/server application
and the STRSRVJOB command, theyre not usually required.
Starts an AS/400 service job. From this service job you can monitor and debug other
OS/400 jobs, such as client/server connections from Delphi. Sometimes used to debug
AS/400 batch jobs. Format:
DSPFFD FILE(library/file)
STRSRVJOB (Start
Service Job)
STRSRVJOB JOB(job/user/number)
DSPJOBLOG (Display
Job Log)
Shows the user what has transpired during the running of the job. It logs all commands,
errors, and informational messages. Its a handy source of information about what your job
has done. It can be as brief or as detailed as you want, depending on how you set up the
AS/400 job. Format:
DSPJOBLOG JOB(job/user/number)
WRKACTJOB (Work
with Active Jobs)
STRSQL (Start SQL)
Shows the user what jobs are active on the system. Its a good way to get the job number
for your client/server connection to the AS/400.
Starts the interactive SQL command processor. It provides a full screen for entering SQL
statements. This utility is useful for trying SQL statements directly on the AS/400 before trying
to run them from your client/server application. A nice feature of this screen is the ability to
prompt the SQL statement. By typing in a SQL statement, such as SELECT, then pressing
4, you can build your statement by filling in the fields provided, making it especially
handy if you cant remember the syntax for a complicated SQL statement.
Common Commands
The commands shown in Figure 1 can be entered on
any OS/400 command line. These commands are primarily concerned with debugging, and displaying file
information, file content, and job information. We
wont get into the details of the commands, but well
review them so you have a starting point from which
to learn more. Weve simplified the example calls to the
commands, as most have many parameters. The examples/formats I use should work in most cases, but may
need to be customized for your particular situation.
33
back at the command line. At this point, you must enter the
STRDBG command and hit R to place the service job in
debug mode. The debug command will cause the AS/400 to log
more information to the job log than it normally would. Its in this
extra information that well see the trace information for the SQL
work done by the DB2/400 server on the AS/400.
Conclusion
When looking at the information on the screen, the first thing you
should do is to hit 0. This will display all messages that have
occurred since the beginning of the job. Note that in Figure 6 the
screen capture shows that the job fetches some records from the
server, closes the cursor, prepares a new SQL statement, opens a
new cursor, and then fetches more records. It also tells you that it
considered all access paths (Indexes) and that it chose the access
path of file MASSU1. This is important information. What if you
thought the query would be using a different index? This could
severely impact the performance of your SQL statement.
The next question you want to ask is why it chose that index over
others. To see the details of that particular item, move the cursor
down to that line and hit 1. This will display a screen similar to
that shown in Figure 7. The details tell you why each index was not
used to complete the query. The second part of the screen, which
cannot be seen in the screen capture, explains what each of the reason codes mean.
Figure 8 is the detail of the next message from Figure 6, and it tells
you why that particular index was chosen for the query. This type of
detail is available where appropriate, and can be of great help in
telling exactly what the AS/400 has done with the SQL statement
your application sent to it.
34
ormally, Im not a big fan of large component sets. I prefer third-party solutions
with a small number of components to master. You could say I like my solutions
focused. Although I did purchase some of the large component collections for my work
in the past, I did it for a specific control or two and found I didnt use the suite for anything other than the component that initially caught my attention.
Installation
Raize went the extra mile with the installation program for this product. It prompts you for a user
name and company, the serial number supplied by
Raize, and the Delphi version(s). My computer has
Delphi 3 and Delphi 4 installed, but I opted to
install the components for Delphi 4 only.
The installation program copies the required packages to disk, registers the components with Delphi,
updates your library path (I wish more third-party
tools would do that), and even combines its Help
35
The Raize List tab includes several general-purpose list, combo box,
and tree view controls that provide additional visual properties, such
as checkboxes associated with the items (TRzCheckList,
TRzCheckTree), or the ability to have multiple columns for every
item (TRzTabbedListBox). TRzEditListBox allows the editing of items
by the user at run time.
Its obvious that Raize Software took the time to fix some of the common annoyances of Delphis standard components: How often do
you place a TPanel on a form and immediately proceed to clear its
Caption property? The TRzPanel component comes with a blank caption by default not a huge improvement by itself, but definitely
an indication of the attention to detail that is part of this library.
Data Controls
If your form contains many edit controls that need to share a custom frame option, its a hassle to change them at design time (and
even worse at run time). Fortunately, the TRzFrameControler component can be used to iterate through all the components on the
form and set the frame attributes property. Thus, you can easily
change between a classic Windows look and your custom frame
look, based on user preferences.
The standard components include button components that can be
linked to a drop-down menu, list and combo boxes with the frame
options (and other display properties), progress bar, track bar, and
other replacements to the standard Delphi components that I wont
mention here because of space limitations.
Other notable components are the TRzSplitter, which creates splits by
defining a container with two sections (vertical or horizontal), instead
of the standard Delphi method of dropping panels with a splitter
between them. This solution makes it easy to create custom splitter
areas, and each part of the two parts in the splitter container is used to
host other components. You can easily drop another splitter on one of
the splitter parts and cascade your splitter to create complicated frames.
The TRzToolbar component makes the design of toolbars easy and
fun. A set of common glyphs is available from the component editor, so you dont have to hunt for images for your buttons. The
WrapControls property can be used to wrap the toolbar buttons
automatically as the user changes its size. A set of standard toolbar
controls (button, button with drop-down menu, or spacer) are
included to make toolbar construction easy. Another advantage is
that this control doesnt require a Microsoft DLL, as the standard
TCoolbar component does.
The status bar replacement (TRzStatusBar) provides standard status bar panes that are always a pain to implement. The available
resources, the state of the keys, a progress bar, and other options
are available. Again, this is not the kind of component that will
36
List Controls
The data components included with RC2 are either simple extensions to the regular Delphi DB Controls with the Raize frame and
display options (e.g. TRzDBLabel, TRzDBEdit, TRzDBMemo, etc.)
or database versions of controls included in the standard Raize tab,
such as TRzDBStatusPane, which can be used as a data-aware pane
in the status bar component described earlier.
As most of the components in this package, these can get their frame
settings from the frame controller mentioned previously, and
when appropriate provide easy-to-use visual component editors.
So Whats Missing?
TRzBMPButton is a button created from a set of bitmaps you provide for states such as Up, Up and Focused, Down, Disabled, etc.
TRzAnimator animates a group of images in an image list
great for small animations that dont require the use of an AVI
or MPEG file.
TRzMeter displays a set of values in several formats yet
another example of a component thats not going to make or
break your application, but the cool graphic options make it
nice to have.
37
Raize Software should consider adding grid components to future releases (standard and data-aware). I
would not expect a do-it-all, cook-and-sing grid like
some of the tools available on the market, but a simple descendant of the Delphi grid with some of the
frame and display capabilities that are RC2s claim to
fame would be nice. If the resident wizard would also allow you
to use the Raize components as the in-place editors, I would be
in UI heaven.
Conclusion
If you cant guess that I like this package, I obviously failed in the
task of writing this review. Its different from most third-party tools I
use; it has no component that for me makes it a must-have,
but it features many components that I use all the time. I just wish I
had it when I started my current development project.
Ron Loewy is a software developer for HyperAct, Inc. He is the lead developer of
eAuthor Help, HyperActs HTML Help authoring tool. For more information about
HyperAct and eAuthor Help, contact HyperAct at (515) 987-2910 or visit
http://www.hyperact.com.
File | New
Directions / Commentary
his was my fourth Inprise/Borland Conference, and it was the most enjoyable so far. Among other things, a conference can be a barometer of the company sponsoring it of its direction, priorities, and health. After I highlight
some of the more memorable events at the conference, I will attempt to read the tea leaves and discuss possible indicators of the future for Inprise/Borland.
At the core of each years conference are excellent tutorials, technological presentations, vendor showcases, and birds-of-a-feather sessions. One highpoint for me this year was a birds-of-a-feather session
that Mark Miller asked me to moderate entitled Can Borlands
Third-Party Vendors Survive? This standing-room-only session was
as provocative as we had hoped it would be, and gave me the opportunity to return to issues related to developer ethics Ive discussed
previously in this column.