RSS
热门关键字:  数据挖掘  数据仓库  商业智能  人工智能  搜索引擎

BuildingActiveXControlswithDelphi3

来源: 作者:unkonwn 时间:2005-08-19 点击:

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:

  1. Build a normal Delphi component, based on TWinControl. Make sure the control is installed in your control palette.
  2. 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.
  3. 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.
  4. Build the project. You now have an ActiveX control.
  5. 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:

  1. Make sure TButton is installed on the palette. This is easy since it"s the way Delphi comes installed.
  2. 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.
  3. 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.
  4. Build the project. There is now a file called "ButtonXControl.ocx" on your machine.
  5. 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:delphi3ButtonXControl.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:

  1. A project file (ButtonXControl.dpr).
  2. 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.
  3. A type library (.tlb) file that is imported into the project.
  4. 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(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}"]

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 reac

最新评论共有 0 位网友发表了评论
发表评论
评论内容:不能超过250字,需审核,请自觉遵守互联网相关政策法规。
匿名?