Distributed COM
Overview
The Distributed
Component Object Model (DCOM) allows you to share objects easily over a
network. In this chapter, you will see how to use BCB to implement DCOM. In
particular, you will see how to create two applications that can control
one another across a network and how to create distributed database
applications. In the process of describing this technology, I will also
show you how to create local OLE Automation objects.
BCB and Windows NT 4.0
provide full support for DCOM. You can also easily add DCOM client support
to Windows 95. If you want, you can also add DCOM server support to Windows
95, though this option has some drawbacks and limitations.
The following subjects
are covered in this chapter:
At the time of this
writing, DCOM is not built into Windows 95, although it is built into
Windows NT 4.0. To add DCOM support to Windows 95, you can download Windows
95 DCOM from the Microsoft Web server. You should start looking for it in
the OleDev section: www.microsoft.com/oledev. Alternatively, you can
purchase a product called OLE
What Is
DCOM?
Before I begin
describing the fairly simple technical steps involved in implementing DCOM,
perhaps I should talk about this technology from a high level so that all
the key ideas will be clear to everyone. If you already understand DCOM and
just want to see how to implement it in BCB, you can skip this section.
Distributed COM is
important because it allows applications to talk to one another across a
network. In particular, it allows you to share objects that reside on two
separate machines. You therefore can create an object in one application or
DLL and then call the methods of that object from an application or DLL
that resides on a different computer.
DCOM is built on top of
COM, which is the technology that underlies both OLE and ActiveX. The
relationship between COM and OLE is a bit confusing, and the boundaries
separating the two technologies seem to shift at times, depending on the
vagaries of the Microsoft marketing machine. In general, I can safely say
that OLE is a subset of COM. That is, everything that is part of OLE is
also part of COM, but not everything that is part of COM is a part of OLE. Many
people, however, use the words "COM" and "OLE"
virtually interchangeably, and indeed, the two technologies are very
closely bound together.
COM is simply a
specification for defining an object hierarchy. In particular, it lays out
a set of rules for defining objects that can be used across applications
and languages. DCOM extends this specification to allow objects on separate
machines to talk to one another.
One of the most
important aspects of this technology is that it allows you to distribute
the load of a task across several machines. For example, if you have a
complex database query to run, you can use DCOM to ask an object on a
separate machine to run it. That way, your current processor will not have
to expend any clock cycles on the task, nor will any large database-
related tools be loaded into memory on your own machine. You are therefore
free to continue playing DOOM or Quake while your server loses clock cycles
and precious RAM to your background task.
You need to understand
that the COM specification, as the name itself implies, is really only a
set of rules for defining an object hierarchy. These rules include defining
the names and methods of many of the key objects in the hierarchy, as well
as the specific techniques for structuring the objects themselves. (When
you think of the definition this way, you might find some value in
regarding OLE as an implementation of certain parts of the COM
specification.)
The COM specification
can be compared to the specification for other object hierarchies such as
VCL, OWL, or MFC. For example, all COM objects descend from a base class
called IUnknown,
just as all VCL objects descend from a base class called TObject. COM supports polymorphism and
encapsulation, and it uses a series of unique interfaces to achieve the
same ends as traditional inheritance in standard object-oriented languages.
Unlike OWL or VCL, however, COM is not tied to any particular language, nor
is it bound by application boundaries.
In short, COM is an
alternative to the VCL, OWL, or MFC that attempts to go these object
hierarchies one better by allowing you to use COM objects across language,
application, and now even machine boundaries. As a result, you can write a
COM object in Object Pascal, extend it in BCB, and then use it in a third
language such as Visual Basic. You can call methods of the object from
inside a single application, from one application that is calling into a
DLL, or from one application that is calling an object in a separate
application. What DCOM brings to the picture is the capability to call
objects that reside in applications located on separate machines. It allows
you to "distribute" the objects across the network. In
particular, this capability allows you to divide up the load of running a
major task across several machines.
If you already
understand COM, then you are ready to use DCOM without any further work. DCOM
works exactly the same way that COM works. You can, in fact, at least
theoretically convert existing COM objects into DCOM objects with no change
to your code. This system works great between two Windows NT machines, but
Windows 95 requires that you switch from Share Level to User Level access,
which might crimp your style in some cases. In particular, User Level
sharing requires that an NT machine or some other source of user access
lists be available on your network.
NOTE: You can switch from Share Level access to
User Level access via the Network applet found in the Control Panel. To
then help configure your server, you can use the DComCfg.exe application freely available
from Microsoft's Web server.
If you don't want to
switch to User Level access, you can choose an alternative that will allow
you to run DCOM as a client service on Windows 95 machines. In that case,
you need to add only a single new parameter to your calls into an OLE
function named CoGetClassObject. I'll explain more in the technical
section of this chapter.
NOTE: Consider these three points: n In most
cases, User Level access controls require that you have an NT machine in
your network. n NT machines don't require any special configuration to be
able to act as a DCOM server. n You cannot remotely launch an OLE server
that resides on a Windows 95 machine. The process must be in memory before
you can call it. This is not true of NT machines, which can automatically
launch a server if it is not already in memory. The upshot of this point is
that you can't set up a DCOM server unless you have an NT machine on your
network, and even under the best circumstances, a Windows 95 box is
crippled as a server. Given these facts, I prefer to have the NT machine
act as my DCOM server and to let the Windows 95 machines act only as
clients. When connecting the two via DCOM for the first time, sign on to
both machines with the same name and password. The simplest way to do this
is to create a new user on the server with the same name and password you
use on your Win95 box. Then sign on to the NT server with this name and
password and sign on to your Windows 95 machine with the same name and
password. Once you get things working this way, then you can try connecting
with more stringent security.
The key point to grasp
here is that DCOM is really nothing more than a new capability added to the
already-existing COM technology. If you have working COM objects, upgrading
them to work with DCOM is easy.
At the time of this
writing (February '97), DCOM has been working on Windows NT 4.0 for over
six months, but Microsoft has only just released DCOM for Windows 95. Microsoft
has stated that, in the future, COM and DCOM will be ported to other
platforms such as UNIX and the Mac.
Why Is
COM Controversial?
Having, in a sense,
made the case for COM and DCOM in the preceding section, I should perhaps
step back for a moment and describe some competing systems. In this
technical chapter, I'm not interested in advocating any particular system. However,
describing the current state of this technology is probably worthwhile so
that you can put this chapter in perspective.
COM is a Microsoft
technology that is competing with similar technologies such as CORBA and
DSOM, which are created by other corporations or groups of corporations. Adherents
of these alternative technologies can rally numerous arguments regarding
who implemented what first and who has designed the most sophisticated
technology. Furthermore, many people have invested themselves heavily in
technologies such as OWL or MFC that can be seen as "competing,"
in some sense, with COM. OWL, MFC, and the VCL don't have the same
capabilities as COM, DSOM, OPENDOC, or CORBA, but you still get a feeling
that the technologies are to some degree competing for the mind share of
contemporary programmers. There is no specific reason that you can't use
COM and VCL in the same program, and indeed that is the approach I take in
this chapter.
My point here is not to
advocate any particular solution, but only to make it clear that this
controversial topic tends to excite strong opinions. If you're considering
using COM in your projects, you might also want to look at CORBA and SOM. Conversely,
if you hear criticisms of COM from other members working in the industry,
you might check to see whether they are so heavily invested in some
alternative technology that they are perhaps somewhat unfairly predisposed
to be critical of COM and DCOM.
DCOM,
IDispatch, Marshaling, and OLE Automation
BCB programmers can use
the IDispatch COM interface to gain easy access to the capabilities of DCOM. This
interface is encapsulated inside the BCB TAutoObject class. If you understand TAutoObject and the theory behind IDispatch and IMarshal, you can probably skip this
section and move on to the next.
OLE Automation is a
technique that allows you to control one application from inside a second
application. In particular, it allows you to control an object placed
inside one application from the code of a second application.
The key to OLE
Automation is a COM object called IDispatch. All OLE technologies are based
on COM, and in this particular case the functionality behind OLE Automation
is implemented by IDispatch. In short, OLE Automation is really just a
marketing term for publicizing the technology found in IDispatch. Or, more charitably, OLE
Automation is an implementation of the IDispatch specification.
IDispatch is not difficult to understand,
but it can be a bit awkward at times to implement. To help simplify the use
of IDispatch,
the VCL has a class called TAutoObject that encapsulates all the functionality of IDispatch inside an easy-to-use and
highly leveraged technology.
In this chapter, I
focus much of the technical content on an analysis of TAutoObject. However, you can automate any
COM object, not just IDispatch. I have chosen to concentrate on this one
technology at the exclusion of others because it provides a simple
workaround to the difficult problem of trying to marshal code and data back
and forth between two applications.
Marshaling is a
COM-specific term for the technique used to transfer data or function calls
back and forth between two applications that reside in separate processes. For
example, if you have to pass a parameter to a function between two
applications, you have to be sure that it is treated properly by both
applications. For example, if you declare the parameter as an Integer in Pascal, that means you're
passing a four-byte ordinal value. How do you express that same concept in
C? How do you do it Visual Basic? The answers to these questions are
expressed in COM by a complex interface called IMarshal that is beyond the scope of
this chapter. Indeed, IMarshal is notorious for being difficult to
implement.
Here is how the
Microsoft documentation defines IMarshal: "`Marshaling' is the
process of packaging data into packets for transmission to a different
process or machine. `Unmarshaling' is the process of recovering that data
at the receiving end. In any given call, method arguments are marshaled and
unmarshaled in one direction, while return values are marshaled and
unmarshaled in the other." This is all good and well. Unfortunately,
as I stated earlier, the IMarshal interface is very hard to implement.
If you're using a
standard COM object, you don't have to implement IMarshal because these interfaces will
be marshaled for you automatically by the system. In other words, if you're
implementing an instance of IDispatch, IUnknown, IClassFactory, IOleContainer, or any other predefined COM
class, you don't have to worry about marshaling. Microsoft will take care
of this job for you. However, if you're creating a custom object of your
own, you need to implement IMarshal or come up with some alternative scheme.
Because of the
complexity of IMarshal, C programmers also generally choose not
to attempt an implementation of IMarshal. Instead, they rely on an
intermediate language called Interface Definition Language (IDL) that can
be compiled into source code by a Microsoft-created program called MIDL.EXE. The IDL is a special language
meant to allow people to define interfaces in a neutral language that can
be compiled into source that can be used by multiple languages such as
Pascal, C, and Visual Basic.
In other words, you can
theoretically write your COM object in C or Pascal, use IDL to define its
interface, and then use MIDL to turn that interface into a set of files
that can be used by any language. In other words, MIDL automatically takes
care of the IMarshal business for you as long as you first describe your interface in IDL.
This approach is quite
reasonable, but I will not treat it in this current book, in part because
BCB does not ship with MIDL. It is, however, available from the Microsoft
SDKs and may be freely available via their Web site. You should also note
that
For now, however, I
will back away from both IMarshal (because it is so complex) and MIDL
(because it doesn't ship with BCB). This situation would appear to leave no
good way to handle DCOM, were it not for the power of IDispatch and TAutoObject. IDispatch is a COM interface designed to
make controlling one application from inside a second application easy. In
implementing this code, Microsoft provided an alternative means for solving
the whole problem of marshaling data between applications. In TAutoObject, the VCL provides a very simple
means of using IDispatch.
Thinking
About IDispatch
Here is how Microsoft
defines IDispatch:
"IDispatch is a COM interface that is designed in such a way that it can call
virtually any other COM interface." In other words, if you put a COM
object in an application, you can call its methods from a second
application by using IDispatch. This way, OLE Automation allows you to
control one application from inside a second application. (In particular, IDispatch was created to help make COM
programming easier from inside the limited confines of a Visual Basic
application.)
To understand why IDispatch works, you need to remember
that marshaling is taken care of for you automatically as long as you're
using an existing COM interface. In other words, you don't have to
implement marshaling for IDispatch because it is a standard COM object, not a
custom object designed by yourself or someone on your team. IDispatch exists to allow you to call the
methods of any legal COM object. In other words, it is designed to solve
the whole problem of marshaling data. As such, it is the perfect solution
for BCB programmers who want to use DCOM without engaging in too much
manual labor.
Before closing this
section, I should emphasize that you don't have to use IDispatch. If you prefer, you can use
other predefined COM objects, or you can implement IMarshal, or you can attempt to use
MIDL. BCB has none of the limitations found in languages like Visual Basic,
so you don't need to be confined to using IDispatch unless you find its relative
simplicity appealing.
Using
TAutoObject to Implement a DCOM Server
Now you're ready to
move away from theoretical issues and to concentrate instead on technical
matters. Ironically, the theory behind this technology is much harder to
understand than the technology itself. In short, in this section and the
next, I outline a simple technique for using DCOM that can be used by any
intermediate-level BCB programmer.
You learned in the
preceding sections that TAutoObject is BCB's wrapper around IDispatch. IDispatch is the COM object that makes
OLE Automation possible. In this section, I show you how to implement OLE
Automation that works not only between two applications, but also between
two applications that reside on separate machines.
If you go to the File
menu in BCB and choose New, you can pop up the Object Repository. On the
first page of the Object Repository is an icon you can select if you want
to create an Automation object. After selecting the Automation Object icon,
you are presented with a dialog, as shown in Figure 27.1. You can fill in the
fields of this dialog as you like, or you can put in the following default
values:
Class Name: TMyDCOM
OLE Class Name: MyProj.MyDCom
Description: My DCOM Object
Instancing: Multiple Instance
When you're done, BCB
spits out two pages of code as shown in Listing 27.1 and Listing 27.2. As
you can see, I have, for the sake of fidelity to the compiler's output and
contrary to habit, left in the mystifying series of dashes inserted by the
compiler. //--------------------------------------------------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------------------------------------------------
#include <vcl\OleAuto.hpp>
#include <vcl\Classes.hpp>
//--------------------------------------------------------------------------
class TMyDCom : public TAutoObject
{
private:
public:
__fastcall TMyDCom();
__automated:
};
//--------------------------------------------------------------------------
#endif
Listing
27.2. The main source file produced by the OLE Automation Wizard.
//--------------------------------------------------------------------------
#include <vcl\vcl.h>
#pragma hdrstop
#include "Unit1.h"
//--------------------------------------------------------------------------
__fastcall TMyDCom::TMyDCom()
: TAutoObject()
{
}
//--------------------------------------------------------------------------
void __fastcall RegisterTMyDCom()
{
TAutoClassInfo AutoClassInfo;
AutoClassInfo.AutoClass = __classid(TMyDCom);
AutoClassInfo.ProgID = "MyProj.MyDCom";
AutoClassInfo.ClassID = "{FCB9F540-87FF-11D0-BCD7-0080C80CF1D2}";
AutoClassInfo.Description = "My DCOM Object";
AutoClassInfo.Instancing = acMultiInstance;
Automation->RegisterClass(AutoClassInfo);
}
//--------------------------------------------------------------------------
#pragma startup RegisterTMyDCom
//---------------------------------------------------------------------------
The RegisterTMyDCOM procedure is used to register
your object with the system--that is, to list it in the Registry. The
details of this process are described in the section called
"Registration Issues." For now, you need only take note of the ClassID assigned to your object because
you will need this ID when you try to call the object from another machine,
as described in the next section.
The act of registering
the object is not something you necessarily have to understand because it
will occur automatically whenever you run the client application of which TMyDCOM is a part.
NOTE: The object will be registered repeatedly,
whenever you run the program, which ensures that you will find it easy to
register the object, while simultaneously requiring very little overhead in
terms of system resources. If you move the application to a new location,
you can register this change with the system by running it once. This
capability guarantees that the old items associated with your CLSID will be
erased, and new items will be filled in their place. Registering a class ID
multiple times does not mean that you will end up with multiple items in
the Registry because each registration of a CLSID will overwrite the
previous registration. All OLE servers worth their name provide this
service. For example, Word and Excel update the Registry each time they are
run.
Besides the
registration procedure, the other key part of the code generated by the
Automation expert is the class definition found at the top of the header:
class TMyDCom : public TAutoObject
{
private:
public:
__fastcall TMyDCom();
__automated:
};
This code has two
sections, one called private and the other called automated. In the __automated section, you can declare
methods or properties that you want to call across program or machine
boundaries. In other words, any methods or properties that you declare in
this space will automatically be marshaled for you by the underlying IDispatch object encapsulated by TAutoObject.
Consider the following
code fragments:
class TSimpleDCOM : public TAutoObject
{
private:
public:
virtual __fastcall TSimpleDCOM();
__automated:
AnsiString __fastcall GetName();
int __fastcall Square(int A);
};
AnsiString __fastcall TSimpleDCOM::GetName()
{
return "SimpleDCOM";
}
int __fastcall TSimpleDCOM::Square(int A)
{
return A * A;
}
This object has two
methods: one that states the name of the object and one that can square an
integer. These two methods are declared in the automated section of the object, so they
can be accessed from another program via another program.
The TSimpleDCOM object exports two methods that IDispatch will automatically marshal for
you across application or machine boundaries. You can go on adding methods
to this object as you like. Any data that you want to add to the object
should go in the private section, and any methods or properties
that you don't want to export should also go in the private section. All methods that you
want to call from inside another application should go in the automated section. You should declare
these exported methods as __fastcall.
Some limits to the
marshaling will be done for you by IDispatch. In particular, the following
types are legal to use in the declarations for the methods or properties in
the automated section:
int,
float,
double,
Currency,
TDateTime,
AnsiString,
WordBool
Short
String
unsigned short
Variant
The following types are
illegal to use in the declarations for the methods or properties in the automated section:
arrays
char *
void *
structs
For additional
information, see the "Automating properties and methods" section
in the online help for the VCL.
The apparent
limitations created by the lack of support from IDispatch for custom types can be
considerably mitigated by an intelligent use of variant arrays. These
structures can be so helpful that I have added a section later in this
chapter called "Using Variant Arrays to Pass Data" to describe
their use.
The complete source for
a simple DCOM server is shown in Listing 27.3 through Listing 27.6. Notice
that OleAuto is included in this project. This unit is essential to OLE Automation
programming with the VCL. ///////////////////////////////////////
// SimpleObject.h
// EasyDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef SimpleObjectH
#define SimpleObjectH
#include <vcl\oleauto.hpp>
#include <vcl\Classes.hpp>
class TSimpleDCOM : public TAutoObject
{
private:
public:
virtual __fastcall TSimpleDCOM();
__automated:
AnsiString __fastcall GetName();
int __fastcall Square(int A);
};
#endif
Listing
27.4. The main source file of an OLE Automation object.
///////////////////////////////////////
// SimpleObject.cpp
// EasyDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#undef RegisterClass
#include "SimpleObject.h"
int Initialization();
static int Initializer = Initialization();
__fastcall TSimpleDCOM::TSimpleDCOM()
: TAutoObject()
{
}
AnsiString __fastcall TSimpleDCOM::GetName()
{
return "SimpleDCOM";
}
int __fastcall TSimpleDCOM::Square(int A)
{
return A * A;
}
void __fastcall RegisterTSimpleDCOM()
{
TAutoClassInfo AutoClassInfo;
AutoClassInfo.AutoClass = __classid(TSimpleDCOM);
AutoClassInfo.ProgID = "EasyDCOM.SimpleDCOM";
AutoClassInfo.ClassID = "{E2674A60-2DF2-11D0-92C5-000000000000}";
AutoClassInfo.Description = "Easiest possible DCOM program";
AutoClassInfo.Instancing = acMultiInstance;
Automation->RegisterClass(AutoClassInfo);
}
int Initialization()
{
RegisterTSimpleDCOM();
return 0;
}
Listing
27.5. The header for the main source file for the EasyDCOM OLE server.
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
class TForm1 : public TForm
{
__published:
TLabel *Label1;
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing
27.6. The main source file for the EasyDCOM OLE server.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
This program is meant
to be run from a client. As such, it has no controls on it and no public
interface other than the OLE object itself. I do, however, give the main
form a distinctive look, as you can see in Figure 27.2. Of course, there is no
reason that a single program could not simultaneously have an OLE server
interface and a set of standard controls. For example, Word and Excel are
both OLE servers, and standard applications run through a set of menus and
other controls. In fact, the same application can work as a server, a
standard application, and as a client.
Note that, by using two
different approaches, you can ensure that the application is registered
each time it is run. One technique involves including an initialization
procedure:
int Initialization()
{
RegisterTSimpleDCOM();
return 0;
}
The second technique,
shown earlier in the chapter, involves a pragma:
#pragma startup RegisterTSimpleDCom
Both technologies
achieve the same effect. As a rule, you don't have to think about this part
of the process because the code will be inserted automatically by the
Automation Wizard. Needless to say, nothing is magic about the Automation
expert, and you can simply create the code yourself by typing it in. In
that case, you are free to use either technique, though the pragma is probably easier to write.
That's all I'm going to
say for now about creating the server side of a BCB DCOM project. Remember
that this code will not work unless you first register the TSimpleDCOM object with the system by
running the server once. After you run the server the first time, you never
have to run it again, as it will be called automatically by the client
program described in the next section. Let me repeat that the whole point
of this exercise is that the client program can be located on a separate
machine.
Creating
the DCOM Client
The GetDCOM program
found on the CD that accompanies this book will call the functions in the
server program described in the preceding section. In particular, GetDCOM
can automatically launch the server program and then call its GetName and Square functions.
NOTE: When I say that GetDCOM can automatically
launch the server, I'm assuming that the server is either on the current
system (in which case, it is launched via COM) or on an NT machine (in
which case, it is launched via DCOM). DCOM cannot launch an application
residing on a remote Windows 95 box.
You can run this
application in two different modes. You can run it as a client to a local
Automation server or as a client to a remote Automation server. If you look
at the main form for the program, shown in Figure 27.3, you can see that it
has three buttons: one for launching the server remotely, one for launching
it locally, and a third that will be used to call a simple function on the
server. The source for the
GetDCOM program is shown in Listing 27.7 and Listing 27.8. This program
uses a routine called CreateRemoteObject that is declared in the CodeBox unit found in the Utils subdirectory on the CD that
accompanies this book. You need to add the CodeBox unit to your project;
otherwise, it will not compile. I do not include the entire CodeBox unit in this chapter, but it is
available on the CD, and I do include the CreateRemoteObject function in its entirety later
in this chapter. Notice also that this project includes the OleAuto unit to call CreateOleObject to retrieve a local instance of IDispatch.
When using this
program, please note that I have hard coded the IP address of my server
into the source. You will need to change this so that it works with your
server. When making the connection between a Windows 95 and Windows NT
machine, you should start by calling from the Windows 95 machine to the
Windows NT machine; that is, put the client on the Windows 95 machine. You
should also start by signing on to both machines with the same name and
password. That way you don't have to worry about security issues on the
server while you are first getting the technology up and running. Also,
give yourself all possible rights on the server. Make yourself an
administrator. ///////////////////////////////////////
// Main.h
// Project: GetDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Buttons.hpp>
class TForm1 : public TForm
{
__published:
TBitBtn *GetLocalObjectBtn;
TBitBtn *GetRemoteObjectBtn;
TEdit *Edit1;
TBitBtn *SquareBtn;
void __fastcall GetLocalObjectBtnClick(TObject *Sender);
void __fastcall GetRemoteObjectBtnClick(TObject *Sender);
void __fastcall SquareBtnClick(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private:
Variant V;
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing
27.8. The main source file for the GetDCOM application.
///////////////////////////////////////
// Main.cpp
// Project: GetDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <vcl\OleAuto.hpp>
#include <vcl\ole2.hpp>
#include <initguid.h>
#pragma hdrstop
#include "Main.h"
#include "codebox.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
CoInitialize(NULL);
}
void __fastcall TForm1::GetLocalObjectBtnClick(TObject *Sender)
{
V = CreateOleObject("EasyDCOM.SimpleDCOM");
ShowMessage(V.OleFunction("GetName"));
}
DEFINE_GUID(ClassID, 0xE2674A60, 0x2DF2, 0x11D0, 0x92,0xC5,
0x00,0x00,0x00,0x00,0x00,0x00);
void __fastcall TForm1::GetRemoteObjectBtnClick(TObject *Sender)
{
Screen->Cursor = crHourGlass;
if (CreateRemoteObject(ClassID, "143.186.149.228", V))
{
ShowMessage(V.OleFunction("GetName"));
}
else
{
ShowMessage("Failed");
}
Screen->Cursor = crDefault;
}
void __fastcall TForm1::SquareBtnClick(TObject *Sender)
{
try
{
ShowMessage(V.OleFunction("Square", Edit1->Text));
}
catch(Exception &E)
{
ShowMessage(E.Message);
}
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
CoUninitialize();
}
The code declares the
CLSID created by the BCB Automation expert in the preceding section of this
chapter:
DEFINE_GUID(ClassID, 0xE2674A60, 0x2DF2, 0x11D0, 0x92,0xC5,
0x00,0x00,0x00,0x00,0x00,0x00);
This is the CLSID
associated with the server half of this DCOM project, and you need to
include it here in the client program. Additional information about CLSIDs
and the registration process will be presented later in this chapter.
NOTE: You need to include the standard Windows
API initguids.h file in projects that use GUIDs.
The actual call to
automate the object is nearly identical to the call you would make if you
wanted to automate an object on your local machine. The only difference is
that you call CreateRemoteOleObject rather than CreateOleObject. Here is the way to call the
object locally:
void __fastcall TForm1::GetLocalObjectBtnClick(TObject *Sender)
{
V = CreateOleObject("EasyDCOM.SimpleDCOM");
ShowMessage(V.OleFunction("GetName"));
}
This code assumes that
the variable V, of type Variant is a field of TForm1. The code calls a built-in
function of the VCL called CreateOleObject. This function takes the ProgID of an object, looks up its
CLSID in the Registry, finds the place on the hard drive where the program
that owns the object is located, launches the program, and retrieves the
object in a Variant.
The code then uses the OleFunction method of the Variant object to call one of the
methods of the OLE server. OleFunction takes one or more parameters, specifying
the name of the function you want to call and any parameters you want to
pass to it. BCB does not have support for named parameters.
Later in the program,
you can call the Square method of the Automation server:
void __fastcall TForm1::SquareBtnClick(TObject *Sender)
{
try
{
ShowMessage(V.OleFunction("Square", Edit1->Text));
}
catch(Exception &E)
{
ShowMessage(E.Message);
}
}
Again, I use the OleFunction method to make the call to the Square method via OleAutomation. As you can see, I wrap the
function call in a try..except block because chances are good that the
user might click the Square button before initializing the object with a
call to CreateOleObject or CreateRemoteOleObject. Notice that this time OleFunction takes two parameters, one
stating the name of the OLE server method to be called and the second
specifying a parameter to be passed to that method.
Here is the method of GetDCOM that I use to summon the remote
server:
void __fastcall TForm1::GetRemoteObjectBtnClick(TObject *Sender)
{
Screen->Cursor = crHourGlass;
if (CreateRemoteObject(ClassID, "143.186.149.228", V))
{
ShowMessage(V.OleFunction("GetName"));
}
else
{
ShowMessage("Failed");
}
Screen->Cursor = crDefault;
}
This function is no
different in substance from the GetDCOM routine that retrieves the
local object. The only difference is that I call CreateRemoteObject rather than CreateOleObject. Remember that you can call CreateOleObject to retrieve remote objects if
you are on an NT machine or if you have set the Windows 95 server machine
into User Access mode via the Network applet in the Control Panel.
NOTE: Let me just reiterate that you need to pass
in the IP address, or server name, of the machine on which your server is
located. Here I type in the IP address of my NT server: 143.186.149.228. You replace this number with
the name or number of your server. If you're confused by the topic of IP
addresses, you might be able to glean some information from the discussion
of TCP/IP in Chapter 8, "Database Basics and Database Tools."
CreateRemoteObject is a custom function I have
written; it looks like this:
BOOL CreateRemoteObject(GUID ClassID, char *Server, Variant &V)
{
Ole2::IClassFactory *ClassFactory;
Ole2::IUnknown *Unknown1;
COSERVERINFO Info;
OLECHAR Dest[MAX_PATH];
int i = MultiByteToWideChar(CP_ACP, 0, Server, -1, Dest, MAX_PATH);
if (i <= 0)
return FALSE;
ClassFactory = NULL;
Info.dwReserved1 = 0;
Info.pAuthInfo = NULL;
Info.dwReserved2 = 0;
Info.pwszName = Dest;
HRESULT hr = CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, &Info,
Ole2::IID_IClassFactory, (void **)&ClassFactory);
OleCheck(hr);
if (ClassFactory != NULL)
{
hr = ClassFactory->CreateInstance(NULL,
Ole2::IID_IUnknown, (void **)&Unknown1);
OleCheck(hr);
V = VarFromInterface(Unknown1);
ClassFactory->Release();
if (VarType(V) != varNull)
return True;
else
return False;
}
return FALSE;
}
This routine is declared
in the CodeBox unit found in the Utils subdirectory on the CD that accompanies
this book. As I stated earlier, you need to add the CodeBox unit to your project;
otherwise, it will not compile. Alternatively, you can simply copy this
routine into your project. However, keeping it in a separate unit makes
sense because you might want to call it from multiple applications.
Whether you understand
this routine is not really important. You can just plug it into your
applications the same way you do CreateOleObject. However, I will talk about it
briefly for those who are interested.
The CreateRemoteObject routine takes three parameters.
The first contains the ID of the object you want to obtain, and the second
contains the name of the server where the object resides. The last
parameter contains a variant that will hold the instance of IDispatch retrieved from the system. (Sometimes
you might have to use the IP address itself rather than the name of the
server.) CreateRemoteObject returns a variant that "contains" a copy of the object
that you want to call. You can use this variant to call all the methods in
the automated section of your object.
Variants are special
BCB types that can contain a wide variety of data types, including OLE
objects. I discussed variants at some length in Chapter 3, "C++Builder
and the VCL."
When you call the
methods of an OLE object off a variant, no runtime checking for the calls
occurs. BCB just assumes you know what you're doing, and if the call fails,
you won't know until runtime. This problem is addressed in Delphi 3, and so
I assume it will be addressed in future releases of C++Builder.
The key call in CreateRemoteOleObject is to CoGetClassObject:
HRESULT hr = CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, &Info,
Ole2::IID_IClassFactory, (void **)&ClassFactory);
OleCheck(hr);
This routine has long
been a part of COM, but it has been altered slightly to support DCOM. Here
is how the routine is currently declared in ObjBase.h:
WINOLEAPI CoGetClassObject(
REFCLSID rclsid, // The ID of the object you want
DWORD dwClsContext, // In process, local or remote server?
LPVOID pvReserved, // Previously reserved, now used for CoServerInfo
REFIID riid, // Usually IID_IClassFactory
LPVOID FAR* ppv); // Where the class factory is returned
The function returns an HRESULT variable containing information
on the outcome of the call. If HRESULT is set to zero, then the call
succeeded. Most other values represent an error in the form of a number. You
can retrieve a human-readable string by passing that number to a VCL
function called OleCheck.
The third parameter to CoGetClassObject, previously reserved, is now
the place where you pass in the name of the server you want to access. The
server is usually designated with either a string or a literal IP address,
such as 143.186.149.111. You would pass in the IP address in the form of a string. That is,
don't try to pass a number; just put the IP address in quotation marks and
pass it in as a string. Here is the new declaration for CoGetClassObject, as found in the MSDN:
STDAPI CoGetClassObject(
REFCLSID rclsid, //CLSID associated with the class object
DWORD dwClsContext, //Context for running executable code
COSERVERINFO * pServerInfo, // Machine on which object is to be instantiated
REFIID riid, //Reference to the identifier of the interface
LPVOID * ppv //Indirect pointer to the interface
);
In particular, here is
the record you pass in for the third parameter:
typedef struct _COSERVERINFO {
DWORD dwSize; // must be set to sizeof(COSERVERINFO)
OLECHAR* pszName; // machine name
} COSERVERINFO;
The first field of this
record is just a version check field that should contain the size of the TCoServerInfo record. The second parameter
contains a Unicode string that has the name of the server or its IP address
embedded in it. Use the MultiByteToWideChar Windows API function to convert a standard
BCB string into a Unicode string:
OLECHAR Dest[MAX_PATH];
int i = MultiByteToWideChar(CP_ACP, 0, Server, -1, Dest, MAX_PATH);
if (i <= 0)
return FALSE;
The call to CoGetClassObject retrieves a ClassFactory. After you have the ClassFactory back from the server, you can
use it to retrieve an instance of the object you want to call. What you
retrieve back, of course, is an instance of IDispatch. You can convert this instance
into a variant by calling the BCB routine VarFromInterface, which is found in the OleAuto unit that ships with BCB.
If you want, you can
simplify this call by using CoCreateInstanceEx. CoCreateInstanceEx is superior to CoGetClassObject because it retrieves the object
you want with only one call instead of having to first get the ClassFactory and then call CreateInstance on the ClassFactory. In short, CoCreateInstanceEx executes faster than CoGetClassObject. (Remember, all calls between
objects on separate machines are going to have a considerable overhead
associated with them!) Another advantage of CoCreateInstanceEx is that it takes a MultiQI structure that can contain a
list of multiple objects to retrieve. That way, you can retrieve multiple
objects through a single call. Again, using this method will save
considerable time.
Before I close this
section, let me review the key points covered so far:
Registration
Issues
Before going further, I
want to mention a few issues about CLSIDs and the Registry. If you already
understand the Registry, you can skip this section. I covered some aspects
of the Registry in Chapter 13, "Flat-File, Real-World Databases."
However, I will go over this material again here from the perspective of an
OLE application.
The Registry is a place
where information can be stored. It's a database.
CLSIDs are
statistically unique numbers that can be used by the operating system to reference
an OLE object. CLSIDs are stored in the Registry.
In this case, visiting
the actual perpetrator in its native habitat is probably best. In the
example explained here, I'm assuming that you have a copy of Word loaded on
your system.
To get started, use the
Run menu on the Windows taskbar to launch the RegEdit program that ships
with Windows NT. Just type RegEdit and click OK. Search through the HKEY_CLASSES_ROOT for the Word.Basic entry, as shown in Figure 27.4.
When you find it, you can see that it's associated with the following
CLSID:
{000209FE-0000-0000-C000-000000000046}
This unique class ID is
inserted into the Registry of all machines that contain a valid, and
properly installed, copy of Word for Windows. The only application that
uses this ID is Word for Windows. It belongs uniquely to that application.
Now go further up HKEY_CLASSES_ROOT and look for the CLSID branch. Open
it and search for the CLSID shown above. When you find it, you can see two
entries associated with it: one is called LocalServer, or LocalServer32, and the other is called ProgID. The ProgID is set to word.basic. The LocalServer entry looks something like
this:
C:\WINWORD\WINWORD.EXE /Automation
If you look at this
command, you can begin to grasp how Windows can translate the CLSID passed
to CoGetClassObject into the name of an executable. In particular, Windows looks up the
CLSID in the Registry and then uses the LocalServer32 entry to find the directory and
name of the executable or DLL you want to launch.
Having these kinds of
entries in the registration database does not mean that the applications in
question are necessarily Automation servers. For example, many applications
with LocalServer and ProgID entries are not Automation servers. However, all Automation servers do have
these two entries. Note, further, that this is a reference to the
Automation server in Word, not a reference to Word as a generic
application. It references an Automation object inside Word, not Word
itself. (The Automation object is an instance of IDispatch. It was not created with TAutoObject, but it has all the same
attributes.)
The same basic scenario
outlined here takes place when you call CoGetClassObject and specify the CLSID of an
object on another machine. In particular, Windows contacts the specified
machine, asks it to look up the CLSID in the Registry, and then marshals
information back and forth between the two machines.
CLSIDs are said to be
statistically unique. You can create a new CLSID by calling CoCreateGuid. The following code shows one
way to make this call:
CoInitialize(NULL);
CoCreateGuid(GUID);
// eventually you should call CoUninitialize;
The code shown here
begins by calling CoInitialize, which is usually unnecessary in BCB
because the OLE2 unit
will call this function automatically when your program is launched; that
is, it will do so if you include OLE2 in the uses clause of one of your units.
CoCreateGuid is the call that retrieves the
new CLSID from the system. This ID is guaranteed to be unique as long as
you have a network card on your system. Each network card has a unique
number on it, and this card number is combined with the date and time and
other random bits of information to create a unique number that could only
be generated on a machine with your network card at a particular date and
time. Rumors that the phase of the moon and current age of Bill Gates's
children are also factored in are probably not true. At any rate, the
result is a number that is guaranteed to be statistically unique, within
the tolerance levels for your definition of that word given your faith in
mathematicians in general and Microsoft-based mathematicians in particular.
The StringFromCLSID routine converts a CLSID into a
string. The ParseGuid routine is a custom function I wrote to convert a string of type
{FC41CC90-C01D-11CF-8CCD-0080C80CF1D2}
into a record of type GUID that can be used in a BCB
application as defined in Wtypes.h:
typedef struct _GUID
{
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[ 8 ];
} GUID;
That's all I want to
say about the Registry for now. This subject can appear a bit tricky at
first, but ultimately it is not complicated.
Using
Variant Arrays to Pass Data
BCB enables you to
create variant arrays, which are the VCL version of the safe arrays used in
OLE Automation. You can use variant arrays to pass large chunks of data
back and forth between COM objects. For example, you can pass a bitmap, AVI
file, or text file between two applications using variant arrays. In short,
this type can help you avoid the shortcomings created by the limited types
supported by IDispatch.
Variant arrays (and
safe arrays) are costly in terms of memory and CPU cycles, so you normally
would not use them except in automation or DCOM code, or in special cases
in which they provide obvious benefits over standard arrays. For example,
the database code makes some use of variant arrays.
The Variant class type, found in SysDefs.h and covered in Chapter 3 has
constructors for creating variant arrays:
// constructor for array of variants of type varType
__fastcall Variant(const int* bounds, const int boundsSize,
Word varType);
// constructor for one-dimensional array of type Variant
__fastcall Variant(const Variant* values, const int valuesSize);
If you know the type of
the elements to be used in an array, you can set the VarType parameter to that type. For
example, if you know you're going to be working with integers, you can
write the following:
Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger);
You cannot use varString in the last parameter; instead,
use varOleStr.
Remember that an array of Variant takes up 16 bytes for each member of the
array, and other types might take up less space.
Arrays of Variant can be resized with the VarArrayRedim function:
extern void __fastcall VarArrayRedim(Variant &A, int HighBound);
The variable to be
resized is passed in the first parameter, and the number of elements to be
contained in the resized array is held in the second parameter.
You declare a
two-dimensional array like this:
Variant MyVariant(OPENARRAY(int, (0, 5, 0, 5)), varInteger);
This array has two
dimensions, each with six elements. To access a member of this array, you
write code that looks like the following:
for (i = 0; i < 6; i++)
for (j = 0; j < 6; j++)
MyVariant.PutElement(i * j, i, j);
for (i = 0; i < 6; i++)
{
for (j = 0; j < 6; j++)
{
S = S + " " + MyVariant.GetElement(i, j);
}
S = S + `\r';
}
The following code
fragment shows how to use a one-dimensional array and how to query an array
to find out about its composition:
AnsiString TForm1::GetInfo(Variant &V)
{
int Count, HighBound, LowBound, i;
AnsiString S;
Count = VarArrayDimCount(V);
S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n';
for (i = 1; i <= Count; i++)
{
HighBound = VarArrayHighBound(V, i);
LowBound = VarArrayLowBound(V, i);
S = S + "LowBound: " + IntToStr(LowBound) + `\n';
S = S + "HighBound: " + IntToStr(HighBound) + `\n';
}
return S + `\n';
}
void __fastcall TForm1::bOneDimClick(TObject *Sender)
{
AnsiString S;
int i;
S = "";
Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger);
for (i = 0; i <= 5; i++)
MyVariant.PutElement(i * 2, i);
for (i = 0; i <= 5; i++)
S = S + " " + MyVariant.GetElement(i);
S = GetInfo(MyVariant) + S;
ShowMessage(S);
}
The GetInfo method demonstrates how to work
with a variant array passed as a parameter. Notice that you don't have to
do anything special to access a variant as an array. The type travels with
the variable.
If you try to pass a
variant with a VType of varInteger to this function, BCB raises an
exception when you try to treat the variant as an array. In short, the
variant must have a VType of VarArray; otherwise, the call to GetInfo will fail. You can use the VarType function to check the current
setting for the VType of a variant, or you can call VarIsArray, which returns a Boolean value.
You can use the VarArrayHighBound, VarArrayLowBound, and VarArrayDimCount functions to find out about the
number of dimensions in your array and about the bounds of each dimension. The
following GetInfo function creates a string showing the number of dimensions in a variant
array, as well as the high and low values for each dimension:
AnsiString TForm1::GetInfo(Variant &V)
{
int Count, HighBound, LowBound, i;
AnsiString S;
Count = VarArrayDimCount(V);
S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n';
for (i = 1; i <= Count; i++)
{
HighBound = VarArrayHighBound(V, i);
LowBound = VarArrayLowBound(V, i);
S = S + "LowBound: " + IntToStr(LowBound) + `\n';
S = S + "HighBound: " + IntToStr(HighBound) + `\n';
}
return S + `\n';
}
This routine starts by
getting the number of dimensions in the array. It then iterates through
each dimension, retrieving its high and low values. If you create an array
with the call
Variant MyVariant(OPENARRAY(int, (0, 5, 1, 3)), varInteger);
the GetInfo function produces the following
output if passed MyVariant:
Dimension Count: 2
HighBound: 5
LowBound: 0
HighBound: 3
LowBound: 1
GetInfo raises an exception if you pass
in a variant that causes VarIsArray to return False.
A certain amount of
overhead is involved in working with variant arrays. If you want to process
the arrays quickly, you can use two functions called VarArrayLock and VarArrayUnlock. The first of these routines
returns a pointer to the data stored in an array. In particular, VarArrayLock takes a variant array and
returns a standard Pascal array. For it to work, the array must be
explicitly declared with one of the standard types listed earlier in the
chapter. The type used in the variant array and the type used in the Pascal
array must be identical.
Here is an example of
using VarArrayLock and VarArrayUnlock:
Variant GetArrayData()
{
int i, j;
Variant V(OPENARRAY(int, (1, Max, 1, Max)), varInteger);
for (i = 1; i < Max; i++)
for (j = 1; j < Max; j++)
V.PutElement(i * j, j, i);
return V;
}
void __fastcall TForm1::LockedArray1Click(TObject *Sender)
{
int Data[Max][Max];
int i, j;
Variant V;
V = GetArrayData();
void *P = VarArrayLock(V);
memcpy(Data, P, sizeof(Data));
for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++)
for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = Data[i-1][j-1];
VarArrayUnlock(V);
}
Notice that this code
first locks down the array and then accesses it as a pointer to a standard
array. Finally, it releases the array when the operation is finished. You
must remember to call VarArrayUnlock when you're finished working with the data
from the array:
for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++)
for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = Data[i-1][j-1];
VarArrayUnlock(V);
Remember that the point
of using VarArrayLock and VarArrayUnlock is that they speed access to the array. The actual code you write
is more complex and verbose, but the performance is faster.
If you don't want to
lock down an array, you can still access the data. You have to do so by
brute-force means, however, and can't use vast pointer-manipulation
routines such as memcpy. The following NormalArray1Click method shows how to proceed if
you don't lock down the data:
void __fastcall TForm1::NormalArray1Click(TObject *Sender)
{
int i, j;
Variant V = GetArrayData();
for (i = 1; i < VarArrayHighBound(V, 1); i++)
for (j = 1; j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = V.GetElement(i, j);
}
One of the most useful
reasons for using a variant array is to transfer binary data to and from a
server. If you have a binary file, say a WAV file or an AVI file, you can
pass it back and forth between your program and an OLE server using variant
arrays. Such a situation would present an ideal time for using VarArrayLock and VarArrayUnlock. You would, of course, use VarByte as the second parameter to VarArrayCreate when you're creating the array.
That is, you would be working with an array of Byte and accessing it directly by
locking down the array before moving data into and out of the structure. Such
arrays are not subject to translation while being marshaled across
boundaries.
The next program in
this chapter shows how to pass data back and forth between programs using
this technique. Listing 27.9 and Listing 27.10 contain a single sample
program that encapsulates most of the ideas that you have seen in this
section on variant arrays. The program from which this code is excerpted is
called VarArray, and you can find it in the Chap27 directory on the disk. Some
screen shots from the program are shown in Figure 27.5 and Fig- ure 27.6. FIGURE 27.6. Viewing a two-dimensional array that is
locked down to get fast access to its data.
Listing
27.9. The header for the VarArray program. VarArray is designed to show how
to use variant arrays.
///////////////////////////////////////
// Main.cpp
// Project: VarArray
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include "Grids.hpp"
#include <vcl\Menus.hpp>
class TForm1 : public TForm
{
__published:
TStringGrid *Grid;
TMainMenu *MainMenu1;
TMenuItem *Options1;
TMenuItem *CreateOneDimensionalArray1;
TMenuItem *CreateTwoDimensionalArray1;
TMenuItem *NormalArray1;
TMenuItem *LockedArray1;
void __fastcall bOneDimClick(TObject *Sender);
void __fastcall bTwoDimClick(TObject *Sender);
void __fastcall NormalArray1Click(TObject *Sender);
void __fastcall LockedArray1Click(TObject *Sender);
private:
AnsiString GetInfo(Variant &V);
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 27.10. The main source file for
the VarArray program.
///////////////////////////////////////
// Main.cpp
// Project: VarArray
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma link "Grids"
#pragma resource "*.dfm"
#define Max 13
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
AnsiString TForm1::GetInfo(Variant &V)
{
int Count, HighBound, LowBound, i;
AnsiString S;
Count = VarArrayDimCount(V);
S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n';
for (i = 1; i <= Count; i++)
{
HighBound = VarArrayHighBound(V, i);
LowBound = VarArrayLowBound(V, i);
S = S + "LowBound: " + IntToStr(LowBound) + `\n';
S = S + "HighBound: " + IntToStr(HighBound) + `\n';
}
return S + `\n';
}
void __fastcall TForm1::bOneDimClick(TObject *Sender)
{
AnsiString S;
int i;
S = "";
Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger);
for (i = 0; i <= 5; i++)
MyVariant.PutElement(i * 2, i);
for (i = 0; i <= 5; i++)
S = S + " " + MyVariant.GetElement(i);
S = GetInfo(MyVariant) + S;
ShowMessage(S);
}
void __fastcall TForm1::bTwoDimClick(TObject *Sender)
{
int i, j;
AnsiString S;
Variant MyVariant(OPENARRAY(int, (0, 5, 0, 5)), varInteger);
for (i = 0; i < 6; i++)
for (j = 0; j < 6; j++)
MyVariant.PutElement(i * j, i, j);
for (i = 0; i < 6; i++)
{
for (j = 0; j < 6; j++)
{
S = S + " " + MyVariant.GetElement(i, j);
}
S = S + `\r';
}
S = GetInfo(MyVariant) + S;
ShowMessage(S);
}
Variant GetArrayData()
{
int i, j;
Variant V(OPENARRAY(int, (1, Max, 1, Max)), varInteger);
for (i = 1; i < Max; i++)
for (j = 1; j < Max; j++)
V.PutElement(i * j, j, i);
return V;
}
void __fastcall TForm1::NormalArray1Click(TObject *Sender)
{
int i, j;
Variant V = GetArrayData();
for (i = 1; i < VarArrayHighBound(V, 1); i++)
for (j = 1; j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = V.GetElement(i, j);
}
void __fastcall TForm1::LockedArray1Click(TObject *Sender)
{
int Data[Max][Max];
int i, j;
Variant V;
V = GetArrayData();
void *P = VarArrayLock(V);
memcpy(Data, P, sizeof(Data));
for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++)
for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = Data[i-1][j-1];
VarArrayUnlock(V);
}
This program has two menu items:
Remember that variant arrays are of use only in special circumstances.
They are powerful tools, especially when you're making calls to OLE
automation objects. However, they are slower and bulkier than standard BCB
arrays and should be used only when necessary.
Using Remote Datasets with DCOM
The DataCom directory on
the CD that accompanies this book contains two programs. One is an OLE
Automation server, and the other is an OLE Automation client. I will talk
about the server first. The code for the server is shown in Listing 27.11
through Listing 27.17. The interface for the server isn't very important
from the perspective of this book, but you can see it in Figure 27.7. Note
that the Globals.h and Globals.cpp files used by both the
client and server applications are stored in the client application's
directory. Listing 27.11. The header for the main
module in the DataServer OLE Automation program.
///////////////////////////////////////
// Main.h
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DBGrids.hpp>
#include "Grids.hpp"
#include <vcl\DBCtrls.hpp>
#include <vcl\ExtCtrls.hpp>
class TForm1 : public TForm
{
__published:
TDBGrid *DBGrid1;
TButton *bFillStrGrid;
TStringGrid *Grid;
TButton *bUpdate;
TDBNavigator *DBNavigator1;
void __fastcall bFillStrGridClick(TObject *Sender);
void __fastcall bUpdateClick(TObject *Sender);
private:
public:
__fastcall TForm1(TComponent* Owner);
Variant __fastcall GetData();
WordBool __fastcall DoUpdate(Variant V);
void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip);
};
extern TForm1 *Form1;
#endif
#endif
Listing 27.12. The main source file for
the DataServer application.
///////////////////////////////////////
// Main.cpp
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "DMod1.h"
#pragma link "Grids"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
Variant __fastcall TForm1::GetData()
{
TCustomerRecord *Customer = new TCustomerRecord();
void *P;
DMod->GetCustAry(*Customer);
Variant V(OPENARRAY(int, (0, sizeof(TCustomerRecord))), varByte);
P = VarArrayLock(V);
memcpy(P, Customer, sizeof(TCustomerRecord));
VarArrayUnlock(V);
return V;
}
// This function merely tests GetData to make sure it is working
void __fastcall TForm1::bFillStrGridClick(TObject *Sender)
{
TCustomerRecord *Customer = new TCustomerRecord();
void *P;
int i;
Variant V = GetData();
P = VarArrayLock(V);
memcpy(Customer, P, sizeof(TCustomerRecord));
VarArrayUnlock(V);
Grid->RowCount = Customer->Count;
for (i = 0; i < Customer->Count; i++)
{
Grid->Cells[0][i] = Customer->CustAry[i].CustNo;
Grid->Cells[1][i] = Customer->CustAry[i].Company;
Grid->Cells[2][i] = Customer->CustAry[i].Address;
Grid->Cells[3][i] = Customer->CustAry[i].City;
Grid->Cells[4][i] = Customer->CustAry[i].State;
Grid->Cells[5][i] = Customer->CustAry[i].Zip;
}
}
WordBool __fastcall TForm1::DoUpdate(Variant V)
{
void *P;
TCustomer C;
try
{
P = VarArrayLock(V);
memcpy(&C, P, sizeof(TCustomer));
VarArrayUnlock(V);
ShowMessage("Ok");
DMod->Update(C);
}
catch(...)
{
return False;
}
return True;
}
void __fastcall TForm1::UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip)
{
TCustomer Customer;
strcpy(Customer.CustNo, CustNo.c_str());
strcpy(Customer.Company, Company.c_str());
strcpy(Customer.Address, Address.c_str());
strcpy(Customer.City, City.c_str());
strcpy(Customer.State, State.c_str());
strcpy(Customer.Zip, Zip.c_str());
DMod->Update(Customer);
}
void __fastcall TForm1::bUpdateClick(TObject *Sender)
{
TCustomer Customer;
AnsiString CustNo;
CustNo = "";
InputQuery("CustNo of Record to Edit", "Enter CustNo: ", CustNo);
strcpy(Customer.Company, "Company");
strcpy(Customer.Address, "Address");
strcpy(Customer.City, "City");
strcpy(Customer.State, "State");
strcpy(Customer.Zip, "Zip");
strcpy(Customer.CustNo, CustNo.c_str());
void *P;
Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte);
P = VarArrayLock(V);
memcpy(P, &Customer, sizeof(TCustomer));
VarArrayUnlock(V);
DoUpdate(V);
}
Listing 27.13. The header for the data
module for the DataServer application.
///////////////////////////////////////
// DMod1.h
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef DMod1H
#define DMod1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DBTables.hpp>
#include <vcl\DB.hpp>
#include "Globals.h"
class TDMod : public TDataModule
{
__published:
TTable *CustomerTable;
TFloatField *CustomerTableCustNo;
TStringField *CustomerTableCompany;
TStringField *CustomerTableAddr1;
TStringField *CustomerTableAddr2;
TStringField *CustomerTableCity;
TStringField *CustomerTableState;
TStringField *CustomerTableZip;
TStringField *CustomerTableCountry;
TStringField *CustomerTablePhone;
TStringField *CustomerTableFAX;
TFloatField *CustomerTableTaxRate;
TStringField *CustomerTableContact;
TDateTimeField *CustomerTableLastInvoiceDate;
TDataSource *CustomerSource;
TQuery *UpdateQuery;
private:
public:
__fastcall TDMod(TComponent* Owner);
void GetCustAry(TCustomerRecord &Customer);
void Update(TCustomer Customer);
};
extern TDMod
*DMod;
#endif
Listing 27.14. The data module for the
DataServer application.
///////////////////////////////////////
// DMod1.cpp
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <vcl\bde.hpp>
#pragma hdrstop
#include "DMod1.h"
#pragma resource "*.dfm"
TDMod *DMod;
__fastcall TDMod::TDMod(TComponent* Owner)
: TDataModule(Owner)
{
CustomerTable->Open();
}
void TDMod::GetCustAry(TCustomerRecord &Customer)
{
int i = 0;
Variant V;
Double Num;
CustomerTable->First();
CustomerSource->Enabled = False;
while (!CustomerTable->Eof)
{
Num = CustomerTable->FieldByName("CustNo")->AsFloat;
sprintf(Customer.CustAry[i].CustNo, "%f", Num);
strcpy(Customer.CustAry[i].Company, CustomerTableCompany->AsString.c_str());
strcpy(Customer.CustAry[i].Address, CustomerTableAddr1->AsString.c_str());
strcpy(Customer.CustAry[i].City, CustomerTableCity->AsString.c_str());
strcpy(Customer.CustAry[i].State, CustomerTableState->AsString.c_str());
strcpy(Customer.CustAry[i].Zip, CustomerTableZip->AsString.c_str());
i++;
CustomerTable->Next();
}
Customer.Count = i - 1;
CustomerSource->Enabled = True;
}
void TDMod::Update(TCustomer Customer)
{
float Value;
UpdateQuery->Close();
UpdateQuery->Params->Items[0]->AsString = Customer.Company;
UpdateQuery->Params->Items[1]->AsString = Customer.Address;
UpdateQuery->Params->Items[2]->AsString = Customer.City;
UpdateQuery->Params->Items[3]->AsString = Customer.State;
UpdateQuery->Params->Items[4]->AsString = Customer.Zip;
Value = StrToFloat(Customer.CustNo);
UpdateQuery->Params->Items[5]->AsFloat = Value;
UpdateQuery->ExecSQL();
}
Listing 27.15. The header for the OLE
Automation object in the DataServer application.
///////////////////////////////////////
// DataObject.h
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef DataObjectH
#define DataObjectH
#include <vcl\OleAuto.hpp>
#include <vcl\Classes.hpp>
class TDataServer : public TAutoObject
{
private:
public:
__fastcall TDataServer();
__automated:
AnsiString __fastcall GetName();
Variant __fastcall TDataServer::GetData();
WordBool __fastcall UpdateRecord(Variant V);
void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip);
};
#endif
Listing 27.16. The main source file for
the OLE Automation object in the DataServer application.
///////////////////////////////////////
// DataObject.cpp
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "DataObject.h"
#include "DMod1.h"
#include "Main.h"
__fastcall TDataServer::TDataServer()
: TAutoObject()
{
}
AnsiString __fastcall TDataServer::GetName()
{
return AnsiString("TDataServer: ") + Now();
}
Variant __fastcall TDataServer::GetData()
{
return Form1->GetData();
}
WordBool __fastcall TDataServer::UpdateRecord(Variant V)
{
return Form1->DoUpdate(V);
}
void __fastcall TDataServer::UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip)
{
Form1->UpdateParams(CustNo, Company, Address, City, State, Zip);
}
void __fastcall RegisterTDataServer()
{
TAutoClassInfo AutoClassInfo;
AutoClassInfo.AutoClass = __classid(TDataServer);
AutoClassInfo.ProgID = "DataServer.DataServer";
AutoClassInfo.ClassID = "{34BADDC0-884F-11D0-BCD7-0080C80CF1D2}";
AutoClassInfo.Description = "DCOM DataServer ";
AutoClassInfo.Instancing = acMultiInstance;
Automation->RegisterClass(AutoClassInfo);
}
#pragma startup RegisterTDataServer
Listing 27.17. The Globals unit contains
some declarations used by both the DataServer and the GetData client
applications. It is stored in the GetData directory.
///////////////////////////////////////
// Globals.h
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef GlobalsH
#define GlobalsH
struct TCustomer {
char CustNo[256];
char Company[256];
char Address[256];
char City[256];
char State[256];
char Zip[256];
};
typedef TCustomer TCustAry[100];
struct TCustomerRecord
{
int Count;
TCustAry CustAry;
};
#endif
This program exports the entire Customer table to remote clients. It also allows the clients to edit a particular
row of data. This way, you can both view and edit the data from a database
without ever loading any database tools on your machine.
NOTE: Understanding that
remote datasets give you many of the advantages of the Web without the slow
performance of the Internet and the limited interface capabilities of HTML
is very important. For example, you can access remote datasets via DCOM
without having to load any database tools. All that you need on your system
is the subset of OLE DLLs that concern COM. All these DLLs, and more, are
loaded whenever you launch the Internet Explorer. Both Web browsers (via
ISAPI and CGI) and DCOM give you access to remote datasets. DCOM, however,
is a more efficient, albeit platform-specific, solution. Understanding the ServerData Program
ServerData is fairly long, but the important sections of code are really
fairly brief, and not particularly difficult to understand. I include the
whole program so you can follow the logic of the entire application at your
leisure, but I will focus mostly on a few key elements.
Here is the declaration for the Automation class:
class TDataServer : public TAutoObject
{
private:
public:
__fastcall TDataServer();
__automated:
AnsiString __fastcall GetName();
Variant __fastcall GetData();
WordBool __fastcall UpdateRecord(Variant V);
void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip);
};
#endif
The GetName function is
provided primarily so that you can test your connection to the server. If
you can call GetName, then
you know that you have access to the server.
The GetData function
retrieves a variant array that contains an entire dataset. At any rate, I
grab the key fields from a dataset and iterate through all the records of
the dataset to get the information I need.
The UpdateRecord and UpdateParams functions are used by the
client when it wants to update data on the server. For example, the user
might edit one particular record and then send the edits back to the server
via these functions.
NOTE: At the time of this
writing, the first versions of BCB out of the dock apparently will not
handle UpdateRecord properly, although they will handle UpdateParams.
The problem with the UpdateRecord function has to do with what appears to be a bug in how BCB handles
variants that are passed as parameters. In short, you simply cannot pass a
variant to a procedure or function by value; you must pass it by reference.
OLE Automation cannot handle parameters that are passed by reference; you
must pass them by value. As a result, you cannot pass variants as
parameters between BCB TAutoObject-based
automation clients and servers. You can, however, return a variant from any
BCB method, including BCB Automation methods. The GetData method looks
like this:
Variant __fastcall TDataServer::GetData()
{
return Form1->GetData();
}
As you can see, I delegate the actual implementation of GetData to the main form. This
practice is common in OLE Automation because the Automation object is
supposed to be a wrapper around the built-in functionality of your server.
For example, ServerData provides access to the Customer table. The goal of the Automation server is
simply to export that functionality to other programs. As a result, the
fact that the Automation object would simply wrap methods already existing
in the program makes sense.
The TForm1 implementation
of GetData looks like this:
Variant __fastcall TForm1::GetData()
{
TCustomerRecord *Customer = new TCustomerRecord();
void *P;
DMod->GetCustAry(*Customer);
Variant V(OPENARRAY(int, (0, sizeof(TCustomerRecord))), varByte);
P = VarArrayLock(V);
memcpy(P, Customer, sizeof(TCustomerRecord));
VarArrayUnlock(V);
return V;
}
This method asks the data module to retrieve a custom structure that
contains the data from the Customer table. I will explain how that process works in one moment. For now, just
concentrate on the fact that the GetData method converts the custom structure into a variant array by using VarArrayLock and VarArrayUnlock. This process was
described earlier in the chapter, in the section on the VarArray program.
The custom data structure used by this program consists of an array of TCustomer structures:
struct TCustomer {
char CustNo[256];
char Company[256];
char Address[256];
char City[256];
char State[256];
char Zip[256];
};
typedef TCustomer TCustAry[100];
The program takes this array and hides inside a custom structure that
defines the number of records in the array:
struct TCustomerRecord
{
int Count;
TCustAry CustAry;
};
Clearly, I could find more memory-efficient ways to store this data, but
I wanted to keep this part of the program simple so that you would be able
to follow the logic of the program without getting bogged down by a mass of
irrelevant pointer manipulation. The important point of this program is how
it handles OLE Automation; finding the best way to store data in memory is
really another subject altogether.
After you declare the data structures, you simply need to fill them out
in the data module for the application:
void TDMod::GetCustAry(TCustomerRecord &Customer)
{
int i = 0;
Variant V;
Double Num;
CustomerTable->First();
CustomerSource->Enabled = False;
while (!CustomerTable->Eof)
{
Num = CustomerTable->FieldByName("CustNo")->AsFloat;
sprintf(Customer.CustAry[i].CustNo, "%f", Num);
strcpy(Customer.CustAry[i].Company, CustomerTableCompany->AsString.c_str());
strcpy(Customer.CustAry[i].Address, CustomerTableAddr1->AsString.c_str());
strcpy(Customer.CustAry[i].City, CustomerTableCity->AsString.c_str());
strcpy(Customer.CustAry[i].State, CustomerTableState->AsString.c_str());
strcpy(Customer.CustAry[i].Zip, CustomerTableZip->AsString.c_str());
i++;
CustomerTable->Next();
}
Customer.Count = i - 1;
CustomerSource->Enabled = True;
}
This code simply iterates through the entire dataset, using brute-force
methods to copy the data into the array. Notice that I disable the DataSource for the module so that the
program does not waste time updating the visual display for a program that
is, after all, running on a remote server.
The data module also provides a method for updating the dataset when the
user sends back a record with new data:
void TDMod::Update(TCustomer Customer)
{
float Value;
UpdateQuery->Close();
UpdateQuery->Params->Items[0]->AsString = Customer.Company;
UpdateQuery->Params->Items[1]->AsString = Customer.Address;
UpdateQuery->Params->Items[2]->AsString = Customer.City;
UpdateQuery->Params->Items[3]->AsString = Customer.State;
UpdateQuery->Params->Items[4]->AsString = Customer.Zip;
Value = StrToFloat(Customer.CustNo);
UpdateQuery->Params->Items[5]->AsFloat = Value;
UpdateQuery->ExecSQL();
}
The preceding is just standard TQuery code, of the type that was explained in depth in Chapter 10, "SQL and
the TQuery Object."
The SQL for the UpdateQuery looks like this:
update
Customer
set
Company = :Company,
Addr1 = :Address,
City = :City,
State = :State,
Zip = :Zip
where
CustNo = :CustNo
As you can see, the code will update an existing record given its CustNo. However, this program makes no
provisions for inserting new data. Obviously, adding that functionality to
the program would not be hard, but I have not done so in this example. In
particular, all you would have to do is insert the new record rather than
just update it. You would, however, have to provide a technique for
providing a valid CustNo.
That's all I'm going to say about the ServerData program. You will
probably want to study a few other parts of the program on your own, but
overall this program is not a complex piece of work. One of the great
advantages of DCOM and OLE Automation is that both technologies are easy to
use.
GetData: The Client Program for Remote
Datasets
The GetData application, found on the CD that accompanies this book,
shows how to access a remote dataset from a client application. A simple
menu allows you to retrieve a dataset from either a local OLE Automation
server or from a remote DCOM Automation server. In both cases, the server
is the ServerData application explained in the preceding section of this
chapter.
After the user connects to the data, it is displayed in the main form of
the program, as shown in Figure 27.8. The user can then edit the data in a
custom form, as shown in Figure 27.9. FIGURE 27.9. Editing
a row of data before sending it back to the server.
The code for the GetData program is shown in Listing 27.18 through
Listing 27.21. I do not show the Globals unit here because it was included in the listings for the ServerData
program. I also omit the CodeBox unit, which is found in the Utils directory on the CD that accom- panies this book. I bring in the CodeBox unit because I need to call CreateRemoteObject. I supplied the
full source for CreateRemoteObject previously in the section "Creating the DCOM Client." ///////////////////////////////////////
// Main.h
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include "Grids.hpp"
#include <vcl\Buttons.hpp>
#include <vcl\Menus.hpp>
#include "globals.h"
class TForm1 : public TForm
{
__published:
TStringGrid *Grid;
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *MakeConnection1;
TMenuItem *MakeConnectionLocal1;
TMenuItem *N1;
TMenuItem *Exit1;
TMenuItem *Options1;
TMenuItem *Edit1;
TMenuItem *UpdateData1;
void __fastcall MakeConnectionBtnClick(TObject *Sender);
void __fastcall ShowDataClick(TObject *Sender);
void __fastcall Edit1Click(TObject *Sender);
void __fastcall UpdateData1Click(TObject *Sender);
void __fastcall MakeConnection1Click(TObject *Sender);
private:
Variant V;
TCustomerRecord FCustomerRecord;
void FillGrid();
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 27.19. The main module for the
GetData program.
///////////////////////////////////////
// Main.cpp
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "OleAuto.hpp"
#include "Globals.h"
#include "codebox.h"
#include "EditData1.h"
#include "initguid.h"
#pragma link "Grids"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::MakeConnectionBtnClick(TObject *Sender)
{
V = CreateOleObject("DataServer.DataServer");
ShowDataClick(NULL);
}
DEFINE_GUID(CLSID_IDATASERVER, 0x34BADDC0, 0x884F, 0x11D0,
0xBC,0xD7,0x00,0x80,0xC8,0x0C,0xF1,0xD2);
void __fastcall TForm1::MakeConnection1Click(TObject *Sender)
{
CreateRemoteObject(CLSID_IDATASERVER, "143.186.149.228", V);
ShowDataClick(NULL);
}
void TForm1::FillGrid()
{
int i;
Grid->RowCount = FCustomerRecord.Count;
for (i = 0; i < FCustomerRecord.Count; i++)
{
Grid->Cells[0][i] = FCustomerRecord.CustAry[i].CustNo;
Grid->Cells[1][i] = FCustomerRecord.CustAry[i].Company;
Grid->Cells[2][i] = FCustomerRecord.CustAry[i].Address;
Grid->Cells[3][i] = FCustomerRecord.CustAry[i].City;
Grid->Cells[4][i] = FCustomerRecord.CustAry[i].State;
Grid->Cells[5][i] = FCustomerRecord.CustAry[i].Zip;
}
}
void __fastcall TForm1::ShowDataClick(TObject *Sender)
{
Variant Data;
void *P;
Data = V.OleFunction("GetData");
P = VarArrayLock(Data);
memcpy(&FCustomerRecord, P, sizeof(TCustomerRecord));
VarArrayUnlock(Data);
FillGrid();
}
void __fastcall TForm1::Edit1Click(TObject *Sender)
{
/* if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top]) == mrOk)
{
FillGrid();
Variant Temp = EditData->GetCustomerAsVariant();
V.OleFunction("UpDateRecord", Temp);
} */
if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top]) == mrOk)
{
FillGrid();
EditData->SendCustomerAsStrings(V);
}
}
void __fastcall TForm1::UpdateData1Click(TObject *Sender)
{
ShowDataClick(NULL);
}
Listing 27.20. The EditData module
provides a form for editing an individual record. This is the header file
for the unit.
///////////////////////////////////////// EditData.h
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef EditData1H
#define EditData1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include "globals.h"
class TEditData : public TForm
{
__published:
TLabel *Label1;
TLabel *Label2;
TLabel *Label3;
TLabel *Label4;
TLabel *Label5;
TLabel *Label6;
TBevel *Bevel1;
TEdit *ECompany;
TEdit *EAddress;
TEdit *ECity;
TEdit *EState;
TEdit *EZip;
TEdit *ECustNo;
TBitBtn *BitBtn1;
TBitBtn *BitBtn2;
void __fastcall BitBtn1Click(TObject *Sender);
private:
TCustomer FCustomer;
public:
__fastcall TEditData(TComponent* Owner);
Variant GetCustomerAsVariant();
void GetCustomer();
void FillCustomer();
int EditCustomer(TCustomer &ACustomer);
void SendCustomerAsStrings(Variant &V);
};
extern TEditData *EditData;
#endif
Listing 27.21. The EditData module
provides a form for editing an individual record. You can then send the
updated data back to the server via OLE Automation.
///////////////////////////////////////
// EditData.cpp
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "EditData1.h"
#pragma resource "*.dfm"
TEditData *EditData;
__fastcall TEditData::TEditData(TComponent* Owner)
: TForm(Owner)
{
}
Variant TEditData::GetCustomerAsVariant()
{
void *P;
GetCustomer();
Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte);
P = VarArrayLock(V);
memcpy(P, &FCustomer, sizeof(TCustomer));
VarArrayUnlock(V);
return V;
}
void TEditData::SendCustomerAsStrings(Variant &V)
{
GetCustomer();
V.OleProcedure("UpdateParams", FCustomer.CustNo, FCustomer.Company,FCustomer.Address,
FCustomer.City, FCustomer.State, FCustomer.Zip);
}
void TEditData::GetCustomer()
{
strcpy(FCustomer.CustNo, ECustNo->Text.c_str());
strcpy(FCustomer.Company, ECompany->Text.c_str());
strcpy(FCustomer.Address, EAddress->Text.c_str());
strcpy(FCustomer.City, ECity->Text.c_str());
strcpy(FCustomer.State, EState->Text.c_str());
strcpy(FCustomer.Zip, EZip->Text.c_str());
}
void TEditData::FillCustomer()
{
ECustNo->Text = FCustomer.CustNo;
ECompany->Text = FCustomer.Company;
EAddress->Text = FCustomer.Address;
ECity->Text = FCustomer.City;
EState->Text = FCustomer.State;
EZip->Text = FCustomer.Zip;
}
int TEditData::EditCustomer(TCustomer &ACustomer)
{
FCustomer = ACustomer;
FillCustomer();
int Result = ShowModal();
if (Result == mrOk)
ACustomer = FCustomer;
return Result;
}
void __fastcall TEditData::BitBtn1Click(TObject *Sender)
{
GetCustomer();
}
This program starts out by retrieving the server either locally or
remotely:
void __fastcall TForm1::MakeConnectionBtnClick(TObject *Sender)
{
V = CreateOleObject("DataServer.DataServer");
ShowDataClick(NULL);
}
DEFINE_GUID(CLSID_IDATASERVER, 0x34BADDC0, 0x884F, 0x11D0,
0xBC,0xD7,0x00,0x80,0xC8,0x0C,0xF1,0xD2);
void __fastcall TForm1::MakeConnection1Click(TObject *Sender)
{
CreateRemoteObject(CLSID_IDATASERVER, "143.186.149.228", V);
ShowDataClick(NULL);
}
I described all the code shown here in some depth earlier in the
chapter. Notice that I use the GUID from the OLE server to retrieve the
program from a remote location. As I mentioned earlier, you can use the DComCfg.exe application to access
remote servers using the same techniques used with local servers. However,
you'll experience some drawbacks using this system when Windows 95 is
involved in the equation. Figure 27.10 shows DComCfg.exe running on an NT server. After you connect to the server, you can ask it for a copy of the
dataset from the Customer table and then display the data to the user:
void __fastcall TForm1::ShowDataClick(TObject *Sender)
{
Variant Data;
void *P;
Data = V.OleFunction("GetData");
P = VarArrayLock(Data);
memcpy(&FCustomerRecord, P, sizeof(TCustomerRecord));
VarArrayUnlock(Data);
FillGrid();
}
This code calls the GetData function of the ServerData program. It then locks down the variant array
returned by the server and extracts the custom record from it. This
operation is the reverse of the operation performed in the ServerData,
where you say how to pack the custom data into a variant array.
The FillGrid method
simply displays the data in a string grid:
void TForm1::FillGrid()
{
int i;
Grid->RowCount = FCustomerRecord.Count;
for (i = 0; i < FCustomerRecord.Count; i++)
{
Grid->Cells[0][i] = FCustomerRecord.CustAry[i].CustNo;
Grid->Cells[1][i] = FCustomerRecord.CustAry[i].Company;
Grid->Cells[2][i] = FCustomerRecord.CustAry[i].Address;
Grid->Cells[3][i] = FCustomerRecord.CustAry[i].City;
Grid->Cells[4][i] = FCustomerRecord.CustAry[i].State;
Grid->Cells[5][i] = FCustomerRecord.CustAry[i].Zip;
}
}
The Cells property of the TStringGrid object allows
you to access the array of data underlying the grid.
Now that the user can see the remote dataset, the only thing left to do
is give him or her a chance to edit it. The following line of code
retrieves the currently selected row from the string grid:
if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top])
== mrOk)
The key point to grasp here is that Grid->Selection.Top designates the currently selected row in the grid.
Inside the TEditData form, only one routine is of any real interest. This routine is called GetCustomerAsVariant:
Variant TEditData::GetCustomerAsVariant()
{
void *P;
GetCustomer();
Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte);
P = VarArrayLock(V);
memcpy(P, &FCustomer, sizeof(TCustomer));
VarArrayUnlock(V);
return V;
}
This code uses the GetCustomer function, which follows, to retrieve the newly edited data from the TEditData form. It then moves the data
into a variant array by first locking down the array and then moving some
bytes around via a call to memcpy.
Here is the simple GetCustomer method used to retrieve the data from the visual controls in the TEditForm:
void TEditData::GetCustomer()
{
strcpy(FCustomer.CustNo, ECustNo->Text.c_str());
strcpy(FCustomer.Company, ECompany->Text.c_str());
strcpy(FCustomer.Address, EAddress->Text.c_str());
strcpy(FCustomer.City, ECity->Text.c_str());
strcpy(FCustomer.State, EState->Text.c_str());
strcpy(FCustomer.Zip, EZip->Text.c_str());
}
If you don't want to pass the data back to the server using a variant
array, you can just pass the strings of the record back directly:
void TEditData::SendCustomerAsStrings(Variant &V)
{
GetCustomer();
V.OleProcedure("UpdateParams", FCustomer.CustNo, FCustomer.Company,
FCustomer.Address, FCustomer.City, FCustomer.State, FCustomer.Zip);
}
This code retrieves the text that the user has edited and then calls the UpdateParams procedure of
the OLE server. UpdataParams will execute a SQL update statement to insert the new data into the Customer table.
That's all I'm going to say about remote datasets. They're one of the
more powerful aspects of DCOM, and I'm sure you can imagine many other ways
to use this technology. If you want to extend this technology with a set of
robust tools, you should look into Entera, a remote client/server
technology provided by Borland.
Summary
In this chapter, you learned how to use BCB to build applications that
take advantage of the Distributed Component Object Model, or DCOM. You have
seen that combining BCB, DCOM, and OLE Automation provides a simple method
for allowing one application to control or use another application that
resides on a second machine.
People who are interested in this field should look at Borland's Entera
and OLEnterprise products, as well as the very powerful OLE-based tools
found in Delphi 3.0. The plan at the time of this writing is for all the
tools in Delphi 3.0 to appear in future versions of BCB.
Windows programmers have seen so many extraordinary technical
developments in the last few years that it's difficult to single out any
one technology and say that it is significantly more important than the
rest. Nevertheless, DCOM appears to be a viable solution to one of the
major problems faced by contemporary programmers. In short, we can now
easily distribute the workload of a particular product across multiple
machines. Just how much impact this technology will have on the industry is
hard to say at this early stage, but DCOM (and related technologies such as
CORBA) certainly have the potential to change the way we build
applications.
VMS Desenvolvimentos
Diversas Dicas, Apostilas, Arquivos Fontes,
Tutoriais, Vídeo Aula, Download de Arquivos Relacionado a Programação em C++
Builder.
|