Introduction
The Structure of an ActiveX Server Library
This course is about how to build an ActiveX control
using Delphi 3. In addition to presenting a tutorial on how to
use Delphi 3's wizards to convert an existing VCL control into
an ActiveX control, the course introduces areas where the control
designer may want to extend the basic code, and provides in-depth
explanation of Delphi's DAX class hierarchy.
The course is a programming tutorial. Students are
expected to be familiar with the Delphi component model, and have
an introductory knowledge of Microsoft COM. While they are not
expected to be familiar with Delphi's interface syntax and class
hierarchies, the course will not cover these in detail even though
they are foundation material. Students are not required to be
familiar with the ActiveX Control specifications and interfaces,
and will be insulated from most of these details. Instead, the
course will focus on Delphi's ActiveX Control class hierarchy
and the wizards used to generate new ActiveX control components.
ActiveX is the brand name for Microsoft's component
object model. Components are objects (in the conventional sense) with
some special capabilities that allow them to be easily combined
into an application. Regardless of the model used to implement
them, components have properties, methods, events, and can load
and save their properties to/from a definition file.
Traditional objects exist only at compile time (where
they are really just symbol table entries in the mind of the compiler)
and at runtime (where they're fully active and interacting with
the user), but components also support design-time operation.
A control in design mode usually is like one at runtime except
it has restricted behavior and its primary methods and events
aren't active. Some design-time controls have augmented capabilities
not available at runtime, such as showing property-editor dialogs.
Controls that are invisible at runtime are visible at design-time,
so the user can interact with the control.
Components make programming easier than traditional
OO languages because they allow the programmer to replace code
statements with specifications. Instead of entering code to create
an object and choosing the right constructor to initialize the
object, you simply drag and drop the object onto its container
(a form or other logical container, like a data module) and set
its creation properties using a property editor. This not only
makes the programming easier, it also makes learning how to use
a new object much easier.
The ActiveX component object model provides all these
basic facilities and varies only slightly from the Delphi object
model's capabilities. For example, Delphi has no property pages
but does have property editors.
The primary technical difference stems from how the
components are written. Delphi's component model assumes language
support from the Object Pascal compiler (or C++Builder) as well
as using helper code from the Delphi runtime library. As a result,
while programming is easy the binary details are less formalized.
This is an intentional choice on the part of Delphi's designers-it's
the compilers responsibility to create the appropriate connecting
code and runtime type information (RTTI) for objects.
The ActiveX component model is designed to be language-independent
and highly version-resilient in object form. The developer (perhaps
with the help of wizards; perhaps a wizard himself) was expected
to write all the code to satisfy the component model's requirements.
As a result, the COM specification provides much less meat, but
is more highly formalized-Microsoft has published three or four
big volumes documenting the specification and updated the spec
several times.
ActiveX defines several component patterns, each
of which has characteristics that make it appealing for specific
situations. Which type you're interested in building depends on
the capabilities the control will be expected to have, and how
you intend to use the component.
ActiveX Control
An ActiveX Control most closely resembles the TWinControl
descendants found in Delphi. The control is intended to be inserted
into a form-like container, it has a window, can be automated
via properties and methods, it can fire events to its container,
save its state to storage provided by its container and restore
a saved stated. ActiveX Controls often provide a set of property
pages that allow the user to edit the saved state, and supports
property inspectors via a property-browsing interface.
Non-visual ActiveX Control
A non-visual control is not visible to the user at
runtime. This component is most closely related to Delphi's TComponent,
which is the base class of all the non-visual controls like TQuery.
The control does not create a window at runtime, but it usually
does at design-time so the user can manipulate it with a mouse.
Data-bound ActiveX Control.
This control is also like a standard ActiveX control,
except that it receives some data from a data source. The data
source is usually a field in a database, but it really could be
from any source. Usually, a specific property (often named "Text"
or "Value") is bound to the current value of the data
source.
Design-time controls
A new feature of ActiveX Controls, this pattern allows
the design-time behavior of a control to be separated from the
runtime code. The two are built into separate libraries, and the
runtime code is usually much smaller than the design-time code.
This shrinks the size of the runtime module, which can be very
beneficial when the code needs to be downloaded over the Internet.
For commercial vendors, it also guarantees that the end-user can't
use the control's design environment without purchasing it.
Internet data controls
These controls, which are in other ways normal ActiveX
Controls, are designed to download data from a remote Internet
site. An example of this might be a picture control with a property
called Source that is an URL string. Internet data controls can
download data asynchronously and update their display as the data
arrives. The picture viewer control starts up empty and displays
the picture in blocks as data blocks arrive over the Net.
Downloadable controls
These controls can be downloaded from an Internet
site and installed locally. They contain a signature that identifies
the control's author. They also implement behavior that determines
whether the control can be trusted to not do something undesirable
if it receives untrusted data or is scripted inside a Web page
that contains untrusted scripts.
ActiveForms
An Active Form is really just an ActiveX-ified representation
of a Delphi TForm. It's primarily intended as a delivery vehicle
for an entire application function within an Intranet, and can
be used to integrate Delphi applications seamlessly with a corporate
Web. ActiveForms can make use of the Delphi VCL to bring up dialogs,
and can connect to remote data or business object servers.
ActiveDocument
An Active Document is really a pair of objects based
on the document-view design pattern, and is the most direct descendant
of the original OLE specification. ActiveDocs contain code to
read a document out of a file and to display and/or edit the data
in a window.
Non-windowed controls
These are extremely lightweight ActiveX controls
that don't create a window handle even though they do have a visual
representation. These correspond to the VCL's TGraphicControl
class in Delphi.
Delphi directly supports building ActiveX Controls, ActiveForms,
and downloadable controls using wizards, the DAX class framework
and its documentation. But since Delphi already comes with its
own complete object model, why would you want to create ActiveX
components? If you pay heed to Microsoft's messaging, there really
are two reasons why you should be interested in building ActiveX
components: Visual Basic and Internet Explorer.
I you prefer to focus on technical reasons to build
an ActiveX component, the language independence is the main thing.
Components built for ActiveX can be used in a wide variety of
programming environments on Windows, not just Delphi or C++Builder.
This means you can build business objects that can be reused across
your organization by people using PowerBuilder, VB or other tools.
Although the ActiveX model offers significant advantages,
there are still ways in which it can be better to stick with Delphi's
native VCL model:
An ActiveX component has no standardized means of
locating one of its peers. Components speak only to their container,
and there exists no standard allowing an object to inquire about
another object. This doesn't mean that controls cannot communicate
with other objects, only that the object's container must a specialized
broker for this process. For example, ActiveX data-bound controls
are given their data by their containers, unlike in Delphi where
the control asks its container to locate a component with the
same name as the DataSource property.
ActiveX relies heavily on property pages for editing
properties, rather than Delphi's notion of property inspectors.
The main difference is that property pages can edit multiple properties,
whereas property editors don't usually edit multiple properties.
ActiveX controls are independent OLE libraries (usually,
DLLs), which means they can't be linked into your program and
must be registered separately. This can make them inconvenient
to install and makes yet another thing the end user must think
about when uninstalling. It also leaves a possible installation
conflict, if two programs install two different versions of the
same library, and the two libraries are inadvertently incompatible
with each other.
Perhaps more importantly, once the ActiveX libraries
are linked you carry all the code around in the DLL even if your
program doesn't use it all. When you use Delphi's native VCL controls,
the smart linker will remove unused code, slimming the resulting
executable significantly.
For the purposes of this class, an ActiveX server
library is really just a Windows DLL, with some specific requirements:
1. It must export the following functions: DllRegisterServer,
DllUnregisterServer, DllGetClassObject and DllCanUnloadNow.
Any other functions are allowed, too.
2. The server contains class factories, one per component
class. An application asks for the appropriate factory by calling
the DllGetClassObject function.
3. It provides the object implementations. Each factory
has a CreateObject method that creates and returns an instance
of a component. The code to implement the component is contained
in the DLL.
4. It contains some special resources in the same DLL. These are:
5. The DLL optionally is stamped with a code signature
identifying the control's author.
Delphi gives you three options when creating an ActiveX
control. You can create a blank library with no controls, add
a control to an existing library, or combine both steps and create
an ActiveX library with an initial control. The reason for this
is that while it is convenient to produce the library and the
control in the same step, you may want to insert multiple controls
into the same library. Also, an ActiveX server library can contain
other kinds of OLE objects besides controls, including property
pages, automation objects, etc.
Since the Delphi Components and ActiveX components
share many semantics and differ only in implementation, making
an ActiveX Control out of a VCL is really just a matter of making
a translation layer on top of the VCL implementation. This layer
makes Delphi properties and methods look like OLE automation methods,
makes Delphi events look like OLE Object events, and makes a VCL control
look like an ActiveX server.
The conversion process involves specifying the automation
and event interfaces and the object's ClassID, and then wrapping the
whole thing up in an ActiveX server library. It also involves
writing short adapter routines for each of the properties, methods
and events to convert OLE-style calls to Delphi and vice versa.
This part is not intellectually challenging but can become time
consuming if your control has a 50-100 properties, methods and
events as many do.
Fortunately, Borland provides an wizard to automate
the entire production of an ActiveX Control from the Delphi VCL.
The wizard uses CodeInsight technology to parse out the
properties, methods and events from a VCL control, then generates
appropriate code for their ActiveX versions.
Thanks to a few new wizards, generating an ActiveX
control is very simple. The basic steps are:
Figure 1. The ActiveX page of the Object Repository
Figure 2. The ActiveX Control Wizard dialog box
In order to explain the code generated by the ActiveX
wizard, we first have to have an example control to examine. In
the following sections, I'll use TButton as a simple example.
Applying the above steps for creating an ActiveX control to the
TButton control yields the following steps:
To understand what the ActiveX control hierarchy
and the wizard actually do, I'll walk through the code generated
by the wizard when you make a new control out of a TButton component.
The wizard generates:
The wizard generates the project file, ButtonXControl.dpr,
shown here. I've inserted commentary into the code, so the best
way to proceed is to read through the code from top to bottom.
Since the library implements an ActiveX component
called 'ButtonX', the line tells Delphi to include the control's
implementation in the project DLL. The {ButtonX: CoClass}
comment tells Delphi that ButtonImpl1 contains a class implementation
that implements the CoClass 'ButtonX' from the type library. This
comment helps the Delphi IDE keep the type library and the object's
implementation in sync when you edit the type library.
This clause specifies that the library exports the
standard ActiveX server functions. These functions are implemented
in ComServ, listed above in the uses clause, so
you don't have to worry about implementing them.
The {$ *.TLB} directive tells the linker to include
the type library file as a resource into the DLL.
This tells the linker to include the project's resources.
This includes at least one toolbar bitmap and optionally a version
information resource.
The {$E ocx} directive tells the linker that the
output filename's extension should be ".OCX".
Before diving into the actual ActiveX control implementation,
it would be worthwhile to describe the general architectural model
used to implement an ActiveX control. In the DAX model, an ActiveX
control is really built with three cooperating objects: the factory,
an ActiveX controller object, and the VCL control. These objects
in turn interact with objects they find in their environment:
Figure 3 shows a diagram of the three objects and
their relationship to each other and their environments.
Figure 3. The DAX object architecture
Delphi's VCL class frameworks provide classes that
implement these relationships: TActiveXFactory, TActiveXControl,
and TWinControl. To implement a class derived fromTWinControl,
you will need to create a new controller class derived from TActiveXControl.
This class is the subject of the next section.
This file contains the main implementation code of
our ActiveX control's ActiveX controller object. This is the object
that defines an automation interface and implements the OLE automation-style
properties, methods and events.
Let's walk through the file and examine the interesting
lines of code:
The ActiveX unit is the unit that defines
all the system interfaces and data types. It's like the OLE2
unit in Delphi 2, except that it's implemented using the new language
features. The OLE2 unit is still around for compatibility
with older code, but any new ActiveX code you write should be
written using ActiveX.
AXCtrls defines the Delphi
ActiveX class hierarchy, also called DAX.
The ButtonXControlLib unit is the Pascal-language
version of the server's type library. It defines all the interfaces
that are available to any object in the server. Normally you would
never edit this file, since it is regenerated from the type library
every time you edit and save the type library. Instead, you should
edit the type library directly using Delphi's Type Library Editor.
This clause defines an object type, TButtonX, that
will be used to implement the controller object. TActiveXControl
is the base class of all ActiveX controls and is implemented in
the AXCtrls unit. The statement also says that the class
implements IButtonX, which is the control's automation interface
defined in the type library.
This private member points to the VCL control. It
gets initialized in the InitializeControl method, below. In code
that appears below, this member is used to get and set properties,
call methods, and do other operations on the VCL object.
This is a pointer to the container's event sink.
IButtonXEvents is a dispinterface, not a dual interface, so what
is stored is really an IDispatch pointer. This value gets set
when the EventSinkChanged method is called, when the control is
inserted or removed from a container. FEventSink can be nil
at various points in your program's execution, so always be aware
of this. In fact, your control could be inserted into a container
that cares nothing about events, so FEventSink could be nil
all the time.
Note that while the DAX class library supports multicast
events, it is far easier to write your control to fire unicast
events. This works fine for ActiveX controls, where the control
is likely to fire events only to its container.
These are declarations for the event handler proxies.
I'll discuss these below, where they are implemented.
The preceding three methods declare implementations
of three overridable virtual methods. These are discussed below.
These methods are property getter methods
for the control. These methods come from the IButtonX interface.
Note that all these automation methods are declared using the
safecall calling convention. Safecall is the ObjectPascal
convention used for declaring dual interface compatible automation
methods. Safecall guarantees that if an exception is thrown
it will be caught and returned as an OLE error, following OLE
calling conventions. It also copies the return value into a return
parameter slot, which is declared as an out parameter in
the type library.
This method is the only public method a TButton exposes
that can be published via OLE automation. Most of TButton's public
methods are internal to VCL's implementation or don't make sense
for the object to provide for automation. For example, the SendToBack
method, which is public in TButton's ancestor class TWinControl,
is a method that should be provided by the container. This method
is first declared in the IButtonX interface.
These methods are the property setter methods
for the control, and are also defined in the IButtonX interface.
They each take a single parameter, which is the new value for
the property.
This method is called after the control is created,
but before the control is shown or inserted into its container.
The main purpose of this method is to establish the connection
between the COM controller object and the VCL object. In the implementation
of this virtual method, the controller gets a pointer to the VCL
object, and then hooks its event proxies into the VCL object.
Control is a property
(of type TWinControl) declared in TActiveXControl, that is initialized
before InitializeControl is called. Of course, it really points
to a TButton control, since that's what we want this ActiveX control
to implement. This line of code coerces the TWinControl pointer
back into a TButton, and stores that pointer in this object.
This code receives the event sink that the container
provided, and remembers it in the FEvents member. FEvents will
be used later to fire events to the object's container. IButtonXEvents
is the control's event dispinterface, which is declared as the
default source interface in the type library.
This protected method starts with no actual implementation
code. It provides you with a means of enumerating the property
pages that you want shown for your control. Since your project
initially has no property pages, this method is left blank, with
instructions on how to fill it in. I'll come back to this topic
later, when we discuss property pages.
The following are typical property getter and setter
methods. All of these follow the same basic pattern: they're safecall
OLE automation method implementations for property get and
set calls. Since the only thing you have to do to set a property
in Delphi is to assign the value, most of these methods look like
the following two methods:
There are a number of cases where the property accessor
code may be more complicated. When the data type of the property
is an integer-derived type, the value parameter
in a setter function is passed in as a SmallInt. Your code needs
to typecast this number into the appropriate Pascal type
type before assigning it to a Pascal property. For example, the
Cursor property is of type TCursor, which is declared:
ActiveX string properties (BSTRs) are compatible
with Delphi's WideString type, and must be used even if the VCL
component exposes a property as an AnsiString. You can do this
by converting the AnsiString value to a WideString in the getter
function, and vice versa in the setter function. (in this example,
remember the TCaption type is a synonym for String).
Another interesting case concerns properties that
have complex OLE types, such as fonts, pictures, and string lists.
Since a font is a separate object that has a dispatch interface,
it can be modified independently of the control, and the control
needs to refresh appropriately when this happens. For example,
you could say in VB:
In this case, the control needs to change its font
to Arial and refresh the display. This necessitates that the Get_Font
method should create and return an OLE object that can expose
the properties of the font as OLE properties. Conversely, setting
the VCL's property in the TFont variable should update the OLE
font.
Fortunately, the DAX library provides builtin functions
for handling these common types. The font property's getter and setter method
implementations demonstrate the
use of the GetOleFont and SetOleFont functions.
You'll notice that the ActiveX Control wizard does
not generate a complete list of all the properties that TButton
publishes to the Delphi form designer. The wizard has decided
that the Height, HelpContext, Hint, Left, Name, ParentFont, ParentShowHint,
PopupMenu, ShowHint, TabOrder, Tag, Top and Width properties should
not be exposed to OLE automation, because they don't make sense
for an ActiveX control. This can be because the
container implements the behavior itself using extended properties
(in the case of position and tabbing properties), because ActiveX
containers do not implement the behavior (ParentFont, HelpContext
and hints), or because the property type is not standard OLE property
type (PopupMenu).
The TActiveXControl class also contributes a number
of property accessors for the properties available to any TWinControl.
These include the following properties: BackColor, Caption, Enabled,
Font, ForeColor, HWnd, TabStop, and Text.
Passing on automation methods to the VCL control
is fairly straightforward. Simply call the appropriate method
in the VCL control that is kept in FDelphiControl. Methods that
have parameters may need to be modified, but this control doesn't
have any.
The following two methods demonstrate how a Delphi-style
event handler forwards an event to the object's container. The
event handlers are connected to the VCL control in the InitializeObject
method, above.
Historical note: When I first implemented this, FEvents
was a Variant from Delphi 2 until the compiler folks had dispinterfaces
working properly. Calling a method on a dispinterface works just
like calling a method on a Variant that contains an IDispatch
pointer, but there are two key differences. The first is performance:
calling through a dispinterface binds the method's dispid at compile-time,
eliminating the sometimes costly GetDispIDsOfNames call.
The second difference is that while ActiveX control
containers expose their event sinks using an IDispatch pointer,
in this case the IDispatch implementation is not required to implement
GetDispIDsOfNames at all! It turns out that some containers
do implement this code, but most do not. If you want your event
firing to work in all containers, you must use dispinterfaces
to fire the events.
OLE events don't have a Sender parameter, so that
parameter is dropped before passing on the event to the parent.
TActiveXControl contributes handlers for common events
like Click, DblClick, KeyDown, KeyPress, KeyUp, MouseDown, MouseMove,
and MouseUp.
This line of code, which gets executed when the library
is loaded, creates the class factory (based on the class, TActiveXControlFactory)
for the control.
ComServer is a global
variable that represents the library itself. Among other things,
the ComServer contains a list of all the factories that have been
created in the library. The other parameters to the factory are:
TButtonX - the ActiveX
implementation class defined above
TButton - the VCL control
class
Class_ButtonX - the ClassID
of the object. This GUID is imported from the ButtonXControlLib
unit, generated from the type library.
ToolbarBitmapID - this is a resource identifier of
a bitmap resource. The wizard generates a bitmap resource for
each control, based on the control's registered icon. ActiveX
containers extract this bitmap to show on their control palettes.
LicenseString - This is blank because we didn't select
MiscControl flags
- these are a combination of OLEMISC_* values that you can use
to request special container behavior. DAX always adds the following
flags to any VCL-derived control: OLEMISC_RECOMPOSEONRESIZE, OLEMISC_CANTLINKINSIDE,
OLEMISC_INSIDEOUT, OLEMISC_ACTIVATEWHENVISIBLE, OLEMISC_SETCLIENTSITEFIRST
The ActiveX type library is a binary file containing
the meta-data for each of the controls listed in an ActiveX library.
It describes the objects in the library, the properties, methods
and events and other interfaces available to each control, and
the user-defined data types used for these. In addition to containing
symbol names and type information, a type library contains a variety
of other information, including human- readable descriptive text,
a reference to a help file and GUIDs for each of these items.
When you compile an ActiveX library, the type library gets copied
into the DLL as a resource, where it can be loaded by any interested
client program.
The ActiveX wizards generate a type library for you
when you first create the ActiveX Control from a Delphi VCL, and
stores the type library in a .TLB file. This library defines all
the properties and methods for your ActiveX control.
For any properties or parameters that convert to
OLE compatible types, the wizard generates properties and parameters
using those OLE types. When your control contains enumeration
properties or parameters, the wizard generates a type declaration
for that enumeration in the type library. In the case where the
data type is a TFont, TPicture, or TStrings, the wizard assigns
the property or parameter an IFont, IPicture or IStrings type
and generates adapter code to convert between the data types.
If your VCL control contains properties or parameters
that aren't standard or adaptable, generally records or non-COM
object types, the wizard will skip that data item. This doesn't
mean that the property doesn't exist, only that you won't be able
to access it through a COM interface.
This is the control's main (dual) automation interface.
The controller object's class will implement all these methods.
This is the dispinterface version of the dual interface
above.
This is the events dispinterface for the control.
The control can fire these events to its container if the container
installs an event sink.
[Note: This file also includes declarations for TButtonX,
which is the VCL class generated when you import the ButtonX
control back into Delphi. For brevity's sake, I've deleted this
from this listing.]
The DAX class hierarchy provides mechanisms for you
to implement or customize certain features of ActiveX controls.
These features include per-property browsing, persistence streaming,
verbs, property pages, ambient properties, and registration.
TActiveXControl defines an immense number of protected
methods, most of which are simply implementations of its interface
methods. Because they're just interface method implementations,
you probably won't need to override any of them. Nevertheless,
they are protected to allow for extending the hierarchy over time,
especially as Microsoft defines new behaviors and changes existing
ones.
This still leaves a few protected methods you might
want to override in specific circumstances. The following sections
describe these situations.
Property browsing support allows a property inspector
to display a property that doesn't normally have a text representation,
such as a font. It also allows the inspector to show a dropdown
list of values that the property can have. You only need to implement
per property browsing where normal variant conversions can't convert
your data to a string or won't do it in the way you want.
Per-property browsing is implemented using three
methods that work together: GetPropertyString, GetProperty Strings,
and GetPropertyValue.
Example: The following code demonstrates how you
can show the Cursor property as a string surrounded by square
brackets.
Bug: There was a bug in the shipping version of Delphi
3.0, which may be fixed by the time you read this. The implementation
of TActiveXControl.GetDisplayString was left blank, when it should
actually pass control to the GetPropertyString method mentioned
above. Fortunately, this is easy to work around, since it simply
requires supplying an implementation for IPerPropertyBrowsing.GetDisplayString.
The following code shows the two places to modify the code to
reimplement GetDisplayString correctly, in the class definition
and in the class implementation.
The default streaming behavior for DAX objects is
to read or write all the property values from the VCL control
using the VCL format. You can add extra information to the persistence
stream by overriding the LoadFromStream or SaveToStream methods.
Be sure to call the inherited method in order to load or save
the control's properties properly.
These methods are defined as:
They read data from or insert data into a persistence
stream. This happens when a control is being restored from a form
file or saved into one.
Working with OLE Streams
In Delphi, the standard streaming class is called
TStream, which has Read, Write and Seek methods. Most Delphi objects
are derived from TPersistent, which is a class that can save its
contents to a TStream. In the OLE world, stream objects provide
an interface called IStream that also has Read, Write and Seek
methods. Delphi 3 provides a class called TOleStream that exposes
an IStream as a TStream. When a TActiveXControl is told to save
its state to a stream via the SaveToStream method, it is given
the IStream as a parameter. If you want to save extra data for
your control to the stream, and the data being saved is a TPersistent-derived
object, you can use the stream adaptor to allow the TPersistent
object to save itself to the IStream. Here is an example of code
that uses TOleStream to save and load a string list in addition
to the control's properties.
A verb is a user-initiated action, generally from
a menu item, that causes the object to do something interesting.
Examples include 'cut', 'execute' or 'run'. You can add verb capabilities
to your control by adding two pieces of code-one to register the
verb and another to execute the verb.
Registering a verb with the object's factory lets
the verb information be copied into the system registry when the
library is installed. This is a requirement of ActiveX because
it allows the object's verbs to be displayed without having the
object loaded in memory first.
To register verbs, call the AddVerb method on the
factory object, as in the following code. Note the ampersand ('&')
in the verb description strings-it is common practice for the
container to display this string in a menu item for the user,
and the ampersand is used to indicate the keyboard selection character
for the menu item.
In the following example code, a verb called 'Click'
is added to the TButtonX control, which will allow the user to
click the button from the container's 'Click' menu item. When
the user selects 'Click', the button's click method is called,
which simulates a button click.
The container is responsible for popping up a menu
that contains the object's verbs, and it then calls the ActiveX
control when the user selects one of the menu items. The DAX class
hierarchy calls the object's PerformVerb method to actually execute
the verb.
The PerformVerb method for the Click example would
be as follows:
A property page is a form embedded in a notebook
control called a property dialog. Like the Delphi Object Inspector,
the property dialog's purpose is to provide the user with a way
to edit the control's properties. Rather than presenting the user
with a long list of property names and values, the property dialog
presents related properties together on a page.
You're not required to provide property
pages for an ActiveX control, but they can be useful if your intended
end-user is a non-technical user.
The property pages that appear inside a property
dialog are just normal windows, but they are also OLE contained
objects, just like ActiveX Controls. Because a property page is
an OLE object, it has to be packaged in an ActiveX library and
registered in the system registry.
The property page doesn't have to actually
exist in the same library as the control that uses it. This is
because some property pages can represent common data types and
be reused across multiple libraries. Delphi provides four basic
property pages for font, color, picture and string properties. The ClassIDs
of these are
To add a property page to your project, you need
to start the ActiveX Property Page wizard from the Object Repository.
This wizard will generate a new unit and a form. For the purposes
of this example, I'll also put an edit control on the form, so
I can use it to edit the Caption property of my control.
The unit for the resulting property page looks like
the following code segment. There are four places to focus on
in this code, which are discussed in the text.
This method is called by the DAX hierarchy in order
to copy data from the OleObject to the page's controls. UpdatePropertyPage
is called when the property dialog first comes up, but can be
called again if the user presses the Undo button (if present).
Here is an example of code to copy the Caption property
from OleObject to a control on the page. (This code assumes you've
placed an edit control on your form, and it's called Edit1).
If you want your property page to show radio buttons
or other complex interacting controls, the code may be more complicated
than this simple example but the principle is the same: get the
value of a property from OleObject and set the value of one or
more controls on the form.
This method does the reverse of the UpdatePropertyPage
method-it copies the data from the controls to OleObject's properties.
This normally happens when the user presses the OK or Apply keys.
Here is an example of code to copy data back from
the edit control to OleObject's Caption property:
This code registers the property page as a COM object
in the ActiveX library. TActiveXPropertyPageFactory is the class
to use when creating the factory for property pages. As with ActiveX
Controls, ComServer is the global variable that represents
the ActiveX library. TPropertyPage1 is the form class declared
above, and Class_PropertyPage1 is the object's ClassID.
Once you've designed a property page, you need to
add the page to the control's list of pages. DAX asks an ActiveX
Control to provide the ClassIDs of all its property pages by calling
the protected DefinePropertyPages method. The method's parameter
is a callback that you can call to add the ClassID of one of your
property pages to the list. When DefinePropertyPages returns,
the property dialog creates the page objects and selects the first
one to the front.
Because this code is executed when the user brings
up the property dialog, you have complete control about which
pages to present to the user. Depending on the user's license
or access rights, you may choose not to show certain pages for
an object.
An ambient property is a property provided by the
control's container. Once the control is inserted in a container,
it can query for the values of the container's ambient properties.
The container can define whatever ambient properties
it wants to expose. ActiveX defines a standard set of ambient
properties, which includes: BackColor, DisplayName, and others.
A container is not required to provide any or all of these properties,
but if it does, Microsoft defines which dispids to use for each.
ActiveX allows you to access ambient properties through the site's IDispatch
interface. Delphi provides a dispinterface, IAmbientDispatch,
which can be used to access the standard interfaces. Since it's
a dispinterface, it's really just
an IDispatch pointer and can be cast to any other dispinterface.
If you're interested in querying the container for a nonstandard
ambient property you'll need to define a new dispinterface that
defines the property and its dispid, then cast FAmbientDispatch
to the new dispinterface. Here's the declaration of IAmbientDispatch:
Example:
The following code responds to the button click, and sets the button's caption
to the DisplayName ambient property. The DisplayName property is usually the
control's name in its container.
When the container changes the value of one of its
ambient properties, it informs the control by calling the object's
OnAmbientPropertyChange method. In Delphi, you can implement your
own handler for this method by overriding the method and re-implementing
the IOleControl interface in your class.
Bug: Ambient Confusion
As if the ActiveX specification wasn't confusing
enough, Microsoft has been confused about how to implement ambient
properties. Certain MS containers, such as Access 97, incorrectly
assume the IDispatch interface it provides for ambient properties
can be the same as the event sink. To make matters worse, some
versions of MFC assume they must be the same IDispatch
pointers. The lesson here is that over the years ActiveX has become
enough of an architectural mess that nobody can ensure-or for
that matter define-complete compliance. What this means for you
as an ActiveX control developer is that you need to be diligent
about testing your control in a variety of containers, and be
prepared to encounter some strange and unexpected behaviors. Even
Microsoft has published controls that work in Internet Explorer
but not in other containers, and each container has different
reactions to the control's incompatibility.
You may want to add new registry entries for your
control. For example, Microsoft recently published a specification,
called component categories, that involves adding registry entries
under the Component Categories key. DAX provides a means of adding registry
entries in the factory class.
Every Delphi COM factory provides
a virtual UpdateRegistry method that gets called when the library
is registered or unregistered.
If you want to add new items to the registry when
the library is registered, derive a new factory class from TActiveXControlFactory
and override its UpdateRegistry method. Then replace the factory
Create call at the bottom of the control's implementation unit
with a call that creates an instance of your new factory class.
Example:
The following code defines a special factory class
called TSpecialFactory that adds a sub-key of the class key called
"SpecialKey". This key has a numeric value, which you set
in the constructor.
To use this class factory, simply replace the existing
class factory creation code in the initialization section of your
ActiveX library: The last line in the example below sets the value
of "SpecialKey" to 1234.
Once you've specified these basic options, you can copy the code to the server by
invoking the Project|Web Deploy command. When you invoke the Web Deploy command,
Delphi generates a web page that refers to your control and copies it to the
HTML destination directory. Delphi then copies the codebase to the web server's
codebase destination directory.
The generated HTML code looks like the following code. Using the above
example directories, Delphi copies it to a file called
c:\https\pages\DAXSamp.HTM.
It the server is a remote Web server that you don't have
file-system access to, you will need to tell Delphi to put the files in a
local directory. Then, you can use your usual web-page deployment method
(for example, FTP or FrontPage) to deploy the code to your server.
Signing your ActiveX control accomplishes two things.
First, it identifies you or your organization as the author of
the code. Second, it gives the recipient of the code the ability
to verify that what they received is what you made and it hasn't
been tampered with.
The key element in code signing is your certificate,
which is a code key assigned to you by a company called a certificate
authority. The certificate
file appears in the form of a .SPC (Software Publisher's Certificate)
file, delivered to you by the certificate authority. You also
need a private key file (.PVK), which you created as part of your application
to the CA for the SPC file.
Microsoft also provides you with a means of creating
untrusted keys for testing purposes. See Microsoft's web site,
http://www.microsoft.com/workshop/prog/security/authcode/codesign.htm
for the paper, "Signing Code with Microsoft's Authenticode".
The process is also described in the Microsoft INET SDK, which
is where you will find the tools required for manufacturing the
untrusted test keys.
Delphi 3 provides a project options page where you
can specify your code signature information, including the file
that contains the credientials and the file that contains the
private key. In the Project|Web Deployment Options dialog, in
the Project page, check the Code Sign Project checkbox. Then,
turn to the Code Signing page and fill in the credentials file
and private key file fields.
The application name and optional company URL fields
will appear in the certificate dialog when the app is downloaded
and verified. You should enter your company's information here.
Security is an extremely important when you're building
an ActiveX for web deployment. While ActiveX controls can be very
powerful and convenient, they can also become your worst nightmare.
As a builder of ActiveX controls, here are some things you should
consider:
When deploying ActiveX controls in an Intranet, here's
some advice:
VB4 provides an open specification for "simple
data bound controls." Such controls are typically controls
that show a single value, like an edit field or a check box. Unlike
the VB complex data binding spec, this standard has been adopted
by a number of vendor, with the notable exception of Delphi itself.
Despite the fact that Delphi doesn't host data-bound ActiveX controls,
it can be used to produce them without too much trouble.
It doesn't take much to make a control data-aware,
once you have a working ActiveX control. A simple data-aware ActiveX
control has a special property that represents its "value".
The property might be called Value, Text, Temperature,
or whatever. This property is marked in the type library with
several flags that indicate that it can be bound to. You can use
the type library editor to set the flags in the control that indicate
how the property is to be bound. Figure 3 shows how this can be
done, by checking the Bindable, Request Edit, Display Bindable
and Default Bindable flags of the property's Attributes pane.
The options tell the container that it can bind
a data source value to this property. If the container chooses to bind
a data source to the bindable property, the two always keep
their values synchronized: when the data source changes value,
the value property, and when the value property changes the data
source changes value.
Figure 4. Type library editor, showing the Caption
property defined as the default bound property.
The value property must also ask its container
for permission to change the value property, before the property
is actually changed. The container can refuse the modification
if desired.
You can implement this relationship easily by taking advantage
of the OnChanging and OnChanged events of your VCL control.
Delphi 3 provides you with an easy way to get started
building ActiveX controls, bu combining a basic class framework
called DAX with the VCL and a set of code-generation wizards.
In this class, I've explained how to convert a VCL control into
an ActiveX control, and then how to add some of the more important
ActiveX features to the control. I've also explained how the Delphi
ActiveX framework is built, and shown how you can extend it. This
is an important skill, because ActiveX is an extremely fluid specification.
Microsoft's OLE web site is at http://www.microsoft.com/oledev.
There's lots of really good stuff there.
Designing and Using OLE Custom Controls, Tom Armstrong,
M&T Press.
Tom maintains a web site at http://www.widgetware.com,
including a comprehensive FAQ.
OLE Controls Inside Out, Adam Denning, Microsoft
Press
Inside OLE, Kraig Brockschmidt, Microsoft Press
More book listings can be found at http://www.microsoft.com/oledev/books.htm
The Java Beans Specification, at http://splash.javasoft.com/beans/spec.html
This paper is a better explanation of component models than Microsoft's.
What OLE is Really About, by Kraig Brockschmidt,
at http://www.microsoft.com/oledev/olecom/aboutole.htm. Best quote: "OLE
is very much like the Coke bottle
"
Vijay Mukhi's site: http://www.neca.com/~vmis/
Vijay writes irreverently about various technologies, including ActiveX.
Best quote: "Microsoft has won the battle for the Internet".
OLE Controls specification, versions 1.0, 1.1 and
2.0, from Microsoft.
OC96 - additions to the OLE controls specification,
from Microsoft.
ActiveX SDK Docs on ActiveX Controls:
http://www.microsoft.com/msdn/sdk/platforms/doc/activex/src/olectrl.htm
Authenticode: see Microsoft's web site at http://www.microsoft.com/security/tech/misf8_2.htm
ActiveDesigner: see http://www.microsoft.com/intdev/sdk/dtctrl
Class hierarchies can be useful reference material
when you can't find why something doesn't work. Look for cryptic comments like
'some containers do this, so we have to bend over backwards here' to
discover where a Microsoft developer has been there before you.
MFC - with Borland C++ 5.01 or Microsoft Visual C++
4.2a.
ActiveX SDK - the BaseCtl framework. See http://www.microsoft.com/intdev/sdk/sdk.htm
ATL 2.x - with Microsoft Visual C++ 5.0
OLE Control Developer's Kit - from Microsoft, in the ActiveX SDK.
(C) Copyright 1997 by Conrad Herrmann.
You may copy, modify, distribute or use for any purpose all ObjectPascal
source code published in this article. All other rights reserved.
The first steps are the easiest: Generating an ActiveX Library
Using the ActiveX Control Wizards
A First Example: Making TButton into an ActiveX control.
What the Wizard Generates
Advanced Features
The ActiveX Project File
The DAX Architecture
The ActiveX Control Implementation File
The Type Library
Per-property Browsing
Web Deployment and Code Signing
Custom Object Streaming
Adding verbs to a control
Adding Property Pages to an ActiveX Control
Accessing Ambient Properties
Adding Custom Registry Entries
Data-aware controls
Conclusion
Further Reading
Introduction
Who should take this class?
This class is for Delphi developers who are interesting
in taking their Delphi programs or business objects across the
Internet or into an Intranet. It is also for programmers who want
to take their Delphi-written components to an audience that uses
VB, PowerBuilder or some other development environment.
What is ActiveX?
Types of ActiveX Controls
Why Should I Build an ActiveX control?
ActiveX Limitations
No containership hierarchy
No property inspectors
No smart linking of components into the executable
The Structure of an ActiveX Server Library
Libraries, Controls, and Multi-control libraries
The first steps are the easiest: Generating an ActiveX Library
Using the ActiveX Control Wizards
A First Example: Making TButton into an ActiveX control.
What the Wizard Generates
The ActiveX Project File
library ButtonXControl;
The library clause defines that the project
produces a DLL file called 'ButtonXControl'.
uses ComServ, ButtonImpl1 in 'ButtonImpl1.pas' {ButtonX: CoClass};
exports DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer;
{$R *.TLB}
{$R *.RES}
{$E ocx}
begin
end.
The DAX Architecture
The ActiveX Control Implementation File
unit ButtonImpl1;
interface
uses Windows, ActiveX, Classes, Controls, Graphics, Menus, Forms, StdCtrls, ComServ, StdVCL, AXCtrls, ButtonXControlLib;
type TButtonX = class(TActiveXControl, IButtonX)
private { Private declarations }
FDelphiControl: TButton;
FEvents: IButtonXEvents;
procedure ClickEvent(Sender: TObject);
procedure KeyPressEvent(Sender: TObject; var Key: Char);
protected
{ Protected declarations }
procedure InitializeControl; override;
procedure EventSinkChanged(const EventSink: IUnknown); override;
procedure DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage); override;
function Get_Cancel: WordBool; safecall;
function Get_Caption: WideString; safecall;
function Get_Cursor: Smallint; safecall;
function Get_Default: WordBool; safecall;
function Get_DragCursor: Smallint; safecall;
function Get_DragMode: TxDragMode; safecall;
function Get_Enabled: WordBool; safecall;
function Get_Font: Font; safecall;
function Get_ModalResult: Integer; safecall;
function Get_Visible: WordBool; safecall;
procedure Click; safecall;
procedure Set_Cancel(Value: WordBool); safecall;
procedure Set_Caption(const Value: WideString); safecall;
procedure Set_Cursor(Value: Smallint); safecall;
procedure Set_Default(Value: WordBool); safecall;
procedure Set_DragCursor(Value: Smallint); safecall;
procedure Set_DragMode(Value: TxDragMode); safecall;
procedure Set_Enabled(Value: WordBool); safecall;
procedure Set_Font(const Value: Font); safecall;
procedure Set_ModalResult(Value: Integer); safecall;
procedure Set_Visible(Value: WordBool); safecall;
end;
implementation
{ TButtonX }
procedure TButtonX.InitializeControl;
begin
FDelphiControl := Control as TButton;
FDelphiControl.OnClick := ClickEvent;
FDelphiControl.OnKeyPress := KeyPressEvent;
These lines bind the VCL events in the control to
this object's event handler proxy methods. This ensures that when
the VCL control fires events, this object will receive them. I'll
describe the detail in the ClickEvent and KeyPressEvent implementations,
but obviously the control will forward the event to its container,
using the ActiveX event protocol.
Bug: The wizard should have generated code
for the standard events, and should have bound OnKeyPress to TActiveXControl.StdKeyPressEvent,
and OnClick to StdClickEvent. By the time you read this, a fix may be available for the wizard.
end;
procedure TButtonX.EventSinkChanged(const EventSink: IUnknown);
begin
FEvents := EventSink as IButtonXEvents;
end;
procedure TButtonX.DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage);
begin
{ Define property pages here. Property pages are defined by calling DefinePropertyPage with
the class id of the page. For example, DefinePropertyPage(Class_ButtonXPage); }
end;
Implementing Property Get and Set Methods
function TButtonX.Get_Cancel: WordBool;
begin
Result := FDelphiControl.Cancel;
end;
procedure TButtonX.Set_Cancel(Value: WordBool);
begin
FDelphiControl.Cancel := Value;
end;
type TCursor = -32768..32767;
The implementation of Cursor's getters and setters look like this:
function TButtonX.Get_Cursor: Smallint;
begin
Result := Smallint(FDelphiControl.Cursor);
end;
procedure TButtonX.Set_Cursor(Value: Smallint);
begin
FDelphiControl.Cursor := TCursor(Value);
end;
function TButtonX.Get_Caption: WideString;
begin
Result := WideString(FDelphiControl.Caption);
end;
procedure TButtonX.Set_Caption(const Value: WideString);
begin
FDelphiControl.Caption := TCaption(Value);
end;
myFont = control.Font
myFont.Facename = 'Arial'
function TButtonX.Get_Font: Font;
begin
GetOleFont(FDelphiControl.Font, Result);
end;
procedure TButtonX.Set_Font(const Value: Font);
begin
SetOleFont(FDelphiControl.Font, Value);
end;
Implementing Methods
procedure TButtonX.Click;
begin
FDelphiControl.Click;
end;
Event Handling
procedure TButtonX.ClickEvent(Sender: TObject);
begin
if
FEvents <> nil then
FEvents.OnClick;
This implementation simply passes on the event to
the container's event sink, if it has been installed. FEvents
was set in the EventSinkChanged method described above. FEvents
is a dispinterface, which means it is really just an IDispatch
pointer.
end;
procedure TButtonX.KeyPressEvent(Sender: TObject; var Key: Char);
var TempKey: Smallint;
begin
In a this case, the parameters expected for OLE events
are not the same as for the Delphi events. In these cases the
event handler proxy may need to massage the event's parameters
before firing the event to the container. In this case, the OnKeyPress
event passes a pointer to a SmallInt to the container, but the
Delphi control passes a pointer to a Char to the event handler.
TempKey := Smallint(Key);
if FEvents <> nil then
FEvents.OnKeyPress(TempKey);
Key := Char(TempKey);
end;
initialization
TActiveXControlFactory.Create(
ComServer, TButtonX, TButton, Class_ButtonX, 1, '', 0);
end.
The Type Library
The Pascal Version of the Type Library
unit ButtonXControlLib;
{ This file represents the pascal declarations
of a type library and will be written during each import or
refresh of the type library editor. Changes to this file will
be discarded during the refresh process. }
{ ButtonXControlLib Library }
{ Version 1.0 }
interface
uses Windows, ActiveX, Classes, Graphics, OleCtrls, StdVCL;
const LIBID_ButtonXControlLib: TGUID = '{B12863C0-A9EA-11D0-A6DF-444553540000}';
const
{ TxDragMode }
dmManual = 0;
dmAutomatic = 1;
{ TxMouseButton }
mbLeft = 0;
mbRight = 1;
mbMiddle = 2;
const
{ Component class GUIDs }
Class_ButtonX: TGUID = '{B12863C3-A9EA-11D0-A6DF-444553540000}';
type
{ Forward declarations }
IButtonX = interface;
DButtonX_ = dispinterface;
IButtonXEvents = dispinterface;
ButtonX = IButtonX;
TxDragMode = TOleEnum;
TxMouseButton = TOleEnum;
{ Dispatch interface for ButtonX Control }
IButtonX = interface(IDispatch)
['{B12863C1-A9EA-11D0-A6DF-444553540000}']
procedure Click; safecall;
function Get_Cancel: WordBool; safecall;
procedure Set_Cancel(Value:WordBool); safecall;
function Get_Caption: WideString; safecall;
procedure Set_Caption(constValue: WideString); safecall;
function Get_Default: WordBool; safecall;
procedure Set_Default(Value: WordBool); safecall;
function Get_DragCursor: Smallint; safecall;
procedure Set_DragCursor(Value: Smallint); safecall;
function Get_DragMode: TxDragMode; safecall;
procedure Set_DragMode(Value: TxDragMode); safecall;
function Get_Enabled: WordBool; safecall;
procedure Set_Enabled(Value: WordBool); safecall;
function Get_Font: Font; safecall;
procedure Set_Font(const Value: Font); safecall;
function Get_ModalResult: Integer; safecall;
procedure Set_ModalResult(Value: Integer); safecall;
function Get_Visible: WordBool; safecall;
procedure Set_Visible(Value: WordBool); safecall;
function Get_Cursor: Smallint; safecall;
procedure Set_Cursor(Value: Smallint); safecall;
property Cancel: WordBool read Get_Cancel write Set_Cancel;
property Caption: WideString read Get_Caption write Set_Caption;
property Default: WordBool read Get_Default write Set_Default;
property DragCursor: Smallint read Get_DragCursor write Set_DragCursor;
property DragMode: TxDragMode read Get_DragMode write Set_DragMode;
property Enabled: WordBool read Get_Enabled write Set_Enabled;
property Font: Font read Get_Font write Set_Font;
property ModalResult: Integer read Get_ModalResult write Set_ModalResult;
property Visible: WordBool read Get_Visible write Set_Visible;
property Cursor: Smallint read Get_Cursor write Set_Cursor;
end;
{ DispInterface declaration for Dual Interface IButtonX }
DButtonX_ = dispinterface ['{B12863C1-A9EA-11D0-A6DF-444553540000}']
procedure Click; dispid 1;
property Cancel: WordBool dispid 2;
property Caption: WideString dispid 3;
property Default: WordBool dispid 4;
property DragCursor: Smallint dispid 5;
property DragMode: TxDragMode dispid 6;
property Enabled: WordBool dispid 7;
property Font: Font dispid 8;
property ModalResult: Integer dispid 9;
property Visible: WordBool dispid 10;
property Cursor: Smallint dispid 11;
end;
{ Events interface for ButtonX Control }
IButtonXEvents = dispinterface
['{B12863C2-A9EA-11D0-A6DF-444553540000}']
procedure OnClick; dispid 1;
procedure OnKeyPress(var Key: Smallint); dispid 2;
end;
implementation
end.
Advanced Features
Per Property Browsing
function GetPropertyString(DispID: Integer; var S: string): Boolean;
When the property inspector displays a property,
it calls this method to see if the property has a display string.
If you want your property to have a display string, add a case
statement for the property, calculate the string you want to show
for the property's current value, and return True. Otherwise,
return False;
function TButtonX.GetPropertyString( id: Integer; var S: String): Boolean;
begin
case id of
10: {Caption}
begin
S := '[' + IntToStr( Get_Cursor ) +']';
Result := True;
end;
else
Result := False;
end;
end;
TButtonX = class(TActiveXControl, IButtonX, IPerPropertyBrowsing)
...
function GetDisplayString(dispid: TDispID; out bstr: WideString):HResult; stdcall;
...
end;
...
function TButtonX.GetDisplayString(dispid: TDispID; out bstr: WideString): HResult;var S: String;
begin
if GetPropertyString( dispid, S ) then
begin
bstr := S;
Result := S_OK;
end
else
Result := E_NOTIMPL;
end;
function GetPropertyStrings(DispID: Integer; Strings: TStrings): Boolean;
GetPropertyStrings and GetPropertyValue work in tandem.
GetPropertyStrings is called to populate a string list with a
list of values that will be shown in a dropdown listbox. Once
the user selects one of these, GetPropertyValues is called to
retrieve the variant value for the selected property.
procedure GetPropertyStrings(DispID: Integer; Strings: Tstrings): Boolean;
begin
if DispID = DISPID_FOO then
begin
Strings.Add('Ten');
Strings.Add('Twenty');
Strings.Add('Thirty');
Result := True;
end
else
Result := False;
end;
procedure GetPropertyValue(DispID, Cookie: Integer; var Value: OleVariant);
begin
if dispid = DISPID_FOO then
Value := Cookie *10;
end;
Custom Object Streaming
procedure
LoadFromStream(const Stream: IStream);
procedure SaveToStream(const Stream: IStream);
var
ExtraInfo: TStringList;
procedure TButtonX.SaveToStream( const Stream: IStream);
var
dStream: TStream;
begin
inherited; dStream := TOleStream.Create( Stream );
try
ExtraInfo.SaveToStream(dStream);
finally
dStream.free;
end;
end;
procedure TButtonX.LoadFromStream( const Stream: IStream );
begin
inherited;
dStream := TOleStream.Create(Stream );
try
ExtraInfo.LoadFromStream(dStream);
finally
dStream.Free;
end;
end;
Adding verbs to a control
const
VERB_CLICK = 100;
initialization
with
TActiveXControlFactory.Create( ComServer, TButtonX, TButton, Class_ButtonX,
1, '', 0) do
begin
AddVerb( VERB_CLICK, '&Click');
end;
end.
procedure TButtonXControl.PerformVerb(Verb: Integer);
begin
case Verb of
VERB_CLICK:
FDelphiControl.Click;
else
inherited PerformVerb(Verb);
end;
end;
Adding Property Pages to an ActiveX Control
Class_DColorPropPage
Class_DFontPropPage
Class_DPicturePropPage
Class_DStringPropPage
Every property page 'edits' an OLE object. When the
property page becomes active, it needs to copy properties from
the object into the controls on the page. When the user clicks
the Apply button, the page needs to copy the properties back to
the OLE object.
unit Unit1;
The page is derived from TPropertyPage, which in
turn is derived from TCustomForm. This means you can design the
form as you would design any other form. TPropertyPage adds an
OleObject property, which references the object your property
page is editing. TPropertyPage also declares the UpdatePropertyPage
and UpdateObject methods, which you override below.
interface
uses SysUtils, Windows, Messages, Classes,
Graphics, Controls, StdCtrls, ExtCtrls, Forms, ComServ, ComObj,
StdVcl, AxCtrls;
type TPropertyPage1 = class(TPropertyPage)
Edit1: TEdit;
private
{ Private declarations}
protected
procedure UpdatePropertyPage; override;
procedure UpdateObject; override;
public
{ Public declarations }
end;
const Class_PropertyPage1: TGUID = '{75ACC806-A9A5-11D0-A6DF-444553540000}';
implementation
{$R *.DFM}
procedure TPropertyPage1.UpdatePropertyPage;
begin
{ Update your controls from OleObject }
Edit1.Text := OleObject.Caption;
end;
procedure TPropertyPage1.UpdateObject;
begin
{ Update OleObject from your controls }
OleObject.Caption := Edit1.Text;
end;
initialization
TActiveXPropertyPageFactory.Create( ComServer,
TPropertyPage1, Class_PropertyPage1);
end.
Connecting the Property Page to an ActiveX Control
procedure TButtonX.DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage);
begin
DefinePropertyPage(Class_PropertyPage1);
end;
Accessing Ambient Properties
IAmbientDispatch = dispinterface
['{00020400-0000-0000-C000-000000000046}']
property BackColor: Integer dispid DISPID_AMBIENT_BACKCOLOR;
property DisplayName: WideString dispid DISPID_AMBIENT_DISPLAYNAME;
property Font: IFontDisp dispid DISPID_AMBIENT_FONT;
property ForeColor: Integer dispid DISPID_AMBIENT_FORECOLOR;
property LocaleID: Integer dispid DISPID_AMBIENT_LOCALEID;
property MessageReflect: WordBool dispid DISPID_AMBIENT_MESSAGEREFLECT;
property ScaleUnits: WideString dispid DISPID_AMBIENT_SCALEUNITS;
property TextAlign: Smallint dispid DISPID_AMBIENT_TEXTALIGN;
property UserMode: WordBool dispid DISPID_AMBIENT_USERMODE;
property UIDead: WordBool dispid DISPID_AMBIENT_UIDEAD;
property ShowGrabHandles: WordBool dispid DISPID_AMBIENT_SHOWGRABHANDLES;
property ShowHatching: WordBool dispid DISPID_AMBIENT_SHOWHATCHING;
property DisplayAsDefault: WordBool dispid DISPID_AMBIENT_DISPLAYASDEFAULT;
property SupportsMnemonics: WordBool dispid DISPID_AMBIENT_SUPPORTSMNEMONICS;
property AutoClip: WordBool dispid DISPID_AMBIENT_AUTOCLIP;
end;
procedure TButtonX.Click;
var
Site: IOleClientSite;
Ambients: IDispatch;
begin
GetClientSite( Site );
if Site <> nil then
Site.QueryInterface(IDispatch, Ambients);
if Ambients <> nil then
begin
Caption := IAmbientDispatch(Ambients).DisplayName;
end;
end;
Tracking Changes to Ambient Properties
Adding Custom Registry Entries
type TSpecialFactory = class(TActiveXControlFactory)
public
constructor Create(ComServer: TComServerObject; ActiveXControlClass: TActiveXControlClass; WinControlClass:
TWinControlClass; const ClassID: TGUID; ToolboxBitmapID:
Integer; const LicStr: string; MiscStatus: Integer; SpecialKeyValue:
Integer); override;
procedure UpdateRegistry(Register: Boolean); override;
protected
FSpecialKeyValue: Integer;
end;
constructor TSpecialFactory.Create(ComServer: TComServerObject;
ActiveXControlClass: TActiveXControlClass; WinControlClass: TWinControlClass;
const ClassID: TGUID; ToolboxBitmapID: Integer;
const LicStr: string; MiscStatus: Integer;
SpecialKeyValue: Integer);
var
TypeAttr: PTypeAttr;
begin
FSpecialKeyValue := SpecialKeyValue;
inherited Create(ComServer, ActiveXControlClass, WinControlClass,
ClassID, ToolboxBitmapID, LicStr, MiscStatus);
end;
procedure TSpecialFactory.UpdateRegistry(Register: Boolean);
var
ClassKey: string;
begin
ClassKey := 'CLSID\' + GUIDToString(ClassID);
if Register then
begin
inherited UpdateRegistry(Register);
CreateRegKey(ClassKey + '\SpecialKey', '', IntToStr(FSpecialKeyValue));
end
else
begin
DeleteRegKey(ClassKey + '\SpecialKey');
inherited UpdateRegistry(Register);
end;
end;
initialization
TSpecialFactory.Create(ComServer, TButtonX, TButton,
Class_ButtonX, 1, '', 0, 1234);
end.
Web Deployment and Code Signing
Web Deployment
ActiveX controls built with Delphi can be deployed to a web site
for downloading into Internet Explorer. Before you can deploy your control,
there are several things you need to figure out:
<HTML>
<H1> Delphi ActiveX Test Page </H1><p>
You should see your Delphi forms or controls embedded in the form below.
<HR><center><P>
<OBJECT
classid="clsid:C72643E3-CAA6-11D0-A6DF-444553540000"
codebase="http://www.mycorp.com/codebase/DAXSamp.ocx#version=1,0,0,0"
width=350
height=250
align=center
hspace=0
vspace=0
>
</OBJECT>
</HTML>
Code Signing
Web Security
Data-aware controls
class TButtonXControl = ...;
...
private
FPropNotifySink: IPropNotifySink;
end;
procedure TButtonXControl.InitializeControl;
begin
FConnectionPoints.CreateConnectionPoint(IPropNotifySink, PropNotifySinkConnect);
FDelphiControl.OnChanged := OnChangedEvent;
FDelphiControl.OnChanging := OnChangingEvent;
end;
procedure TButtonXControl.PropNotifySinkConnect(const Sink: IUnknown);
begin
if Sink <> nil then
OleCheck(Sink.QueryInterface(IPropNotifySink, FPropNotifySink))
else FPropNotifySink := nil;
end;
procedure TButtonXControl.Set_Caption(const Value: WideString);
begin
FDelphiControl.Caption := TCaption(Value);
end;
procedure TButtonXControl.OnChangingEvent( Sender: TObject );
begin
if FPropNotifySink <> nil then
if FPropertyNotifySink.RequestEdit( DISPID_CAPTION ) = S_FALSE then
OleError( CTL_E_SETNOTPERMITTED );
end;
procedure TButtonX.OnChangedEvent( Sender:TObject );
begin
if FPropNotifySink <> nil then FPropNotifySink.OnChanged( DISPID_CAPTION );
end;
Conclusion
Further Reading
Books
White papers
Documentation
Class hierarchies