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.
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 reac
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
数据挖掘研究院

