Introduction
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.
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.
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.
What is ActiveX™?
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.
Types of ActiveX Controls
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.
Why Should I Build an ActiveX control?
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.
ActiveX Limitations
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:
No containership hierarchy
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.
No property inspectors
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.
No smart linking of components into the executable
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.
The Structure of an ActiveX Server Library
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:
a small bitmap that is used to represent the control on a tool palette.
A type library.
Version information.
5. The DLL optionally is stamped with a code signature identifying the control's author.
Libraries, Controls, and Multi-control libraries
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.
The first steps are the easiest: Generating an ActiveX Library Using the ActiveX Control Wizards
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:
Build a normal Delphi component, based on TWinControl. Make sure the control is installed in your control palette.
Go to the ActiveX wizards page in the Object Repository (see Figure 1) and choose "ActiveX Control." Delphi presents you with a wizard page (see Figure 2) where you choose the VCL control to derive from and fill in other information. For the VCL class name, choose the class you just installed. The dialog also presents a number of checkbox options.
When you press OK, the wizard generates the code needed to implement the ActiveX control and adds the code to a project. If the current project is already an ActiveX library, then the wizard adds the control to the current project. If the current project is not an ActiveX project, then the wizard offers to create a new project to contain the ActiveX control.
Build the project. You now have an ActiveX control.
Register the control in the system, using the Run|Register ActiveX Server command. Registering is something you only need to do when you first create the control, or when you change the control's information that would be stored in the registry,
Figure 1. The ActiveX page of the Object Repository
Figure 2. The ActiveX Control Wizard dialog box
A First Example: Making TButton into an ActiveX control.
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:
Make sure TButton is installed on the palette. This is easy since it's the way Delphi comes installed.
Choose File|New, select the ActiveX page and choose ActiveX Control. Press OK. In the wizard dialog, choose TButton as the VCL class name. The wizard automatically fills in the ActiveX name as ButtonX, the implementation unit as ButtonImpl1.pas, and the project name as ButtonXControl.dpr. The control options are all unchecked. Accept these defaults and press the OK button.
The wizard informs you that since the current project is not an ActiveX project, it needs to create a new project. Answer OK. Now the wizard generates all the code for your project and control.
Build the project. There is now a file called "ButtonXControl.ocx" on your machine.
Register the control in the system, using the Run|Register ActiveX Server command. If successful, Delphi pops up a message saying "Successfully registered ActiveX server, 'c:\delphi3\ButtonXControl.ocx.'"
What the Wizard Generates
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:
A project file (ButtonXControl.dpr).
An ActiveX server implementation file that contains:
Automation interfaces and method implementations for the ActiveX control. The interfaces and methods map to those properties and methods of the VCL control that can be mapped to OLE.
Event interfaces and proxy implementations that map from the VCL events to the ActiveX protocols.
Class registry information and a factory object that allows the object to be registered and created by COM.
A type library (.tlb) file that is imported into the project.
A Pascal version of the type library.
The ActiveX Project File
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. library ButtonXControl;
The library clause defines that the project produces a DLL file called 'ButtonXControl'. uses ComServ, ButtonImpl1 in 'ButtonImpl1.pas' {ButtonX: CoClass} ;
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. exports DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer;
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. {$R *.TLB}
The {$ *.TLB} directive tells the linker to include the type library file as a resource into the DLL. {$R *.RES}
This tells the linker to include the project's resources. This includes at least one toolbar bitmap and optionally a version information resource. {$E ocx}
The {$E ocx} directive tells the linker that the output filename's extension should be ".OCX". begin
end.
The DAX Architecture
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:
the factory interacts with CoCreateInstance and other OLE runtime functions, and creates a controller object.
the controller object interacts with an ActiveX container and site, can be automated by an ActiveX script engine, manipulates the VCL control, and forwards events that the VCL control fires to its container site;
the VCL object interacts with and is embedded into its parent window, and fires events back to the controller object.
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.
The ActiveX Control Implementation File
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:
unit ButtonImpl1;
interface
uses Windows, ActiveX, Classes, Controls, Graphics, Menus, Forms, StdCtrls, ComServ, StdVCL, AXCtrls, ButtonXControlLib;
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. type TButtonX = class (TActiveXControl, IButtonX)
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. private { Private declarations }
FDelphiControl: TButton;
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. FEvents: IButtonXEvents;
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. procedure ClickEvent(Sender: TObject);
procedure KeyPressEvent(Sender: TObject; var Key: Char);
These are declarations for the event handler proxies. I'll discuss these below, where they are implemented. protected
{ Protected declarations }
procedure InitializeControl; override ;
procedure EventSinkChanged( const EventSink: IUnknown); override ;
procedure DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage); override ;
The preceding three methods declare implementations of three overridable virtual methods. These are discussed below. 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 ;
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.
procedure Click; safecall ;
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.
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 ;
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. end ;
implementation
{ TButtonX }
procedure TButtonX.InitializeControl;
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. begin
FDelphiControl := Control as TButton;
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. 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;
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. 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); }
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.
end ;
Implementing Property Get and Set Methods
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: function TButtonX.Get_Cancel: WordBool;
begin
Result := FDelphiControl.Cancel;
end ;
procedure TButtonX.Set_Cancel(Value: WordBool);
begin
FDelphiControl.Cancel := Value;
end ;
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: 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 ;
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). function TButtonX.Get_Caption: WideString;
begin
Result := WideString(FDelphiControl.Caption);
end ;
procedure TButtonX.Set_Caption(const Value: WideString);
begin
FDelphiControl.Caption := TCaption(Value);
end ;
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: myFont = control.Font
myFont.Facename = 'Arial'
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.
function TButtonX.Get_Font: Font;
begin
GetOleFont(FDelphiControl.Font, Result);
end ;
procedure TButtonX.Set_Font(const Value: Font);
begin
SetOleFont(FDelphiControl.Font, Value);
end ;
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.
Implementing Methods
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. procedure TButtonX.Click;
begin
FDelphiControl.Click;
end ;
Event Handling
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. 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.
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. 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.
OLE events don't have a Sender parameter, so that parameter is dropped before passing on the event to the parent. TempKey := Smallint(Key);
if FEvents nil then
FEvents.OnKeyPress(TempKey);
Key := Char(TempKey);
end ;
TActiveXControl contributes handlers for common events like Click, DblClick, KeyDown, KeyPress, KeyUp, MouseDown, MouseMove, and MouseUp. initialization
TActiveXControlFactory.Create(
ComServer, TButtonX, TButton, Class_ButtonX, 1, '', 0);
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 end .
The Type Library
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.
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}']
This is the control's main (dual) automation interface. The controller object's class will implement all these methods. procedure Click; safecall ;
function Get_Cancel: WordBool; safecall ;
procedure Set_Cancel(Value:WordBool); safecall ;
function Get_Caption: WideString; safecall ;
procedure Set_Caption( const Value: 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}']
This is the dispinterface version of the dual interface above. 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}']
This is the events dispinterface for the control. The control can fire these events to its container if the container installs an event sink. procedure OnClick; dispid 1;
procedure OnKeyPress( var Key: Smallint); dispid 2;
end ;
[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.] implementation
end .
Advanced Features
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.
Per Property Browsing
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. 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;
Example: The following code demonstrates how you can show the Cursor property as a string surrounded by square brackets. 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 ;
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.
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
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: procedure LoadFromStream( const Stream: IStream); procedure SaveToStream( const Stream: IStream);
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. 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
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. const
VERB_CLICK = 100;
initialization
with
TActiveXControlFactory.Create( ComServer, TButtonX, TButton, Class_ButtonX,
1, '', 0) do
begin
AddVerb( VERB_CLICK, '&Click');
end ;
end.
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: 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
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 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.
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.
unit Unit1;
interface
uses SysUtils, Windows, Messages, Classes,
Graphics, Controls, StdCtrls, ExtCtrls, Forms, ComServ, ComObj,
StdVcl, AxCtrls;
type TPropertyPage1 = class (TPropertyPage)
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. 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 }
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). Edit1.Text := OleObject.Caption;
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. end ;
procedure TPropertyPage1.UpdateObject;
begin
{ Update OleObject from your controls }
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: OleObject.Caption := Edit1.Text;
end ;
initialization
TActiveXPropertyPageFactory.Create( ComServer,
TPropertyPage1, Class_PropertyPage1);
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. end .
Connecting the Property Page to an ActiveX Control
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. procedure TButtonX.DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage);
begin
DefinePropertyPage(Class_PropertyPage1);
end ;
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.
Accessing Ambient Properties
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: 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 ;
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. 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
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.
Adding Custom Registry Entries
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.
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 ;
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. 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:
Where the binary codebase will reside on your server. This could be an URL like "http://www.mycorp.com/code/DAXSamp.ocx".
A directory where Delphi can copy the HTML file. If the HTTP server is running on your machine, this could be a local filename like "c:\https\codebase\".
A directory where Delphi can copy the codebase file. If the HTTP server is running on your machine, this could be a local filename like "c:\https\pages\".
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.
Delphi ActiveX Test Page
You should see your Delphi forms or controls embedded in the form below.
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.
Code Signing
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.
Web Security
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:
Be ethical. Don't build ActiveX controls that are harmful.
Build your control to be tamper-resistant.
Keep close watch on your software certificate. Establish notary procedures for its use. Keep records about what controls you've signed and who signed it, and who had access to the signature key.
Don't give your control privileges over your network. Don't include default passwords, etc.
Set up your server so it knows who downloads the controls and when and where.
When deploying ActiveX controls in an Intranet, here's some advice:
Code signatures do not necessarily mean full security. Even signed, legitimate ActiveX controls have security flaws that can be taken advantage of.
Don't accept HTML pages containing an ActiveX control from anyone you don't trust as well as your least trustworthy staff. If you have a business partner that only publishes its data through an ActiveX, allow only that ActiveX to run, and only in the context of the page they published.
Caveat surfer
Data-aware controls
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. 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
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.
Further Reading
Microsoft's OLE web site is at http://www.microsoft.com/oledev . There's lots of really good stuff there.
Books
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
White papers
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".
Documentation
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
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.