![]() |
![]() |
![]()
| ![]() |
Dynamic Delphi ActiveX / Delphi 3-5
Run-time ActiveX Embedding ActiveX Controls at Run Time
The release of Delphi 3 introduced the ability to use ActiveX controls in Delphi applications. A developer who wants to use an ActiveX control imports it into the Delphi development environment using the Component | Import ActiveX Control menu option in Delphi 4/5. The Delphi import utility creates a Pascal wrapper around the ActiveX control, and adds it to the Delphi component library.
The developer can then drag the control onto a Delphi form and use it in the application. So, it appears that Delphi supports everything one could want to do with an ActiveX control. However, a closer inspection shows that a Delphi application can use an ActiveX control only if it's known to Delphi at compile time. This is different from other forms of COM objects, such as Automation objects that can be used by Delphi with the CreateOleObject procedure in the ComObj.pas system unit. The truth is that an ActiveX control can be instantiated like an Automation object and used in code, but the most important part of the ActiveX control, the visual representation of the functionality it encapsulates, cannot be used. In this article, I'll introduce a relatively simple method to use "unknown" ActiveX controls at run time.
Why Bother? The December, 1999 issue of Delphi Informant contains an article I wrote about an application extension framework via COM interfaces. The article discusses the subjects of creating an application object model, creating a framework for COM-based plug-ins, and integrating the COM plug-ins with the application's menu structure. The plug-ins described in this article perform logic functions by accessing the application's object model. If the plug-in needs to collect information from the user, it displays a modal dialog box that is separate from the forms of the application.
Consider the idea of an application extension framework that needs to include embedded views that are part of the plug-ins created by the application users or third-party vendors: If you want to take advantage of the COM benefits, you need a method to embed these views into your application. A natural fit for COM-based embedded "views" is ActiveX controls.
Let's consider an application that allows the user to manage and edit rich media. Assume a database of rich media elements at a newspaper where media items (pictures, videos, audio clips, etc.) are indexed for easy retrieval by the reporters and researchers that work for the publication. In this article, a researcher might enter a set of keywords and get a collection of hits, a set of articles related to the media element. The researcher then needs to click the different hits, and view/listen to them to determine if they meet his or her needs.
Assume that we wrote this application as a standard Delphi application. We support standard Windows BMP files. We also wrote the support for other popular file formats, such as EPS, GIF, JPEG, and AVI files. Unfortunately, if we want to support new formats, such as MPEG video, RealAudio, or PNG images, we'd need to recompile the application and distribute it to our users. And we'd need to perform the same task every time a new media format needs to be added. In addition to the hassle of recompiling and distributing, we'd also have to contend with increasing application source code size, the need to maintain all the code when a new version of the compiler arrives, and the possibility of introducing bugs when making these changes.
An alternative solution is to define a standard way to register media types with the system, and store the metadata about the media element with the media element. When the element needs to be viewed, an ActiveX control will be embedded within the application and will display the element. When a new media type needs to be added, a new ActiveX control will be written to display this media type, and the ActiveX control will be registered with the system. With this solution, the code pieces for every element are separate from the rest of the system. Every project is smaller in scope, and therefore easier to write and maintain. Different developers can write the different modules without the need to retain similar coding styles, make changes in code written by other people, or cause unintentional harm to code. If we sell our news media indexer application to other users, the users can add support to new media types, or other elements specific to their organization without access to our code. (Consider a publication that uses an internally developed XML DTD to store information about news items, and wants to display this information using a graphical, hierarchical view specific to their needs.)
ActiveX Control = Automation Object + Visual Stuff Before we start investigating the ins and outs of the visual parts of an ActiveX control, it's a good idea to quickly review the COM object's pecking order. Every COM object is an object that implements the IUnknown interface. This interface provides the ability to retrieve other interfaces supported by the object (via the QueryInterface method), and the object's lifetime handling via reference counting.
An Automation object is an object that implements the IDispatch interface. This interface allows non-compiled languages, such as VBScript, JScript, and VBA, to access the object's properties and methods by packing parameters into standard memory structures that are passed to the object. A Delphi-created Automation object also supports a type library, a binary representation (metadata) of the object's properties and methods.
To keep it simple: An ActiveX control is an Automation object that implements a set of interfaces that allow an ActiveX host application to embed the control visually into one of its windows. The control will display itself within the area the application provides for the control, and will control the focus and mouse within this area.
An ActiveX Control in Delphi Clothes Our quest to understand ActiveX controls, and learn how to embed one at run time in an application, starts by inspecting the code generated by Delphi when an ActiveX control is imported. For the purposes of this article, I imported the Microsoft Internet Explorer (WebBrowserOC) control and inspected the code created by Delphi in the ShDocVw_TLB.pas file that Delphi placed in the \Imports sub-directory of the Delphi installation directory. Every ActiveX control you import and inspect will be fine for the purpose of understanding what makes an ActiveX control useable by Delphi.
Looking for the definition of TWebBrowser in the generated file, we realize that the class descends from the TOleControl class. The Delphi 3 and 4 Help files describe TOleControl as follows: "TOleControl is derived from TWinControl and handles the interactions with OLE necessary for using OCX controls in the Delphi environment. It is unlikely that you will ever need to derive a component from this class."
This description is accurate; the code Delphi generates when it imports an ActiveX control creates a VCL wrapper around the control (derived from TWinControl), and allows your Delphi application to interact with the correct OLE embedding interfaces. We must, however, disagree with the claim that it's unlikely that we'll derive a component from this class, as we'll do just that to create our run-time embedding code.
If we continue to inspect the code generated by Delphi for the TWebBrowser class, we can see that it contains two main parts: code specific to the interface implemented by the specific ActiveX control, and maintenance code not defined for this interface. I specifically chose the Microsoft IE control, because the main interface of the control, IWebBrowser2, is well-documented in the Microsoft INetSDK, and it was easy to separate the control-specific functionality (methods such as GoBack, GoHome, and Navigate in this specific control) from code generated by Delphi to manage the OLE interaction.
Usually, when we inspect the code of a specific control we imported, we would only be interested in the code specific to this control and its functionality, and we are happy that Delphi automatically creates all the boring maintenance code for us. Not so in this case; the interesting code, for our purposes, is the code that creates and manages the communication with the OLE interfaces.
In the protected section of the generated code, we see the functions CreateControl and InitControlData, which should be inspected. We should also note the ControlInterface property, defined in the public part of the class definition. These functions include the secret to the way a Delphi ActiveX wrapper interacts with the OLE interfaces.
Inspecting the code further reveals that CreateControl returns the control-specific interface from an internal field named OleObject. InitControlData initializes a ControlData structure, and returns a pointer to it. One of the interesting values defined in the control data structure is the class ID of the ActiveX control. This leads us to investigate the code for TOleControl to better understand what's happening.
TOleControl is defined in the file OleCtrls.pas, which is installed in the \Source\VCL directory of the standard Delphi installation. If we inspect this file, we find that TOleControl is a TWinControl descendant that implements the following interfaces:
What we can learn from this is that the TOleControl descendant is an ActiveX host (or Site in OLE speak) that talks to the ActiveX control and tells it where it can display itself. Looking further into the code, we can see that OleObject (mentioned previously) is an IOleObject interface reference.
Let's now inspect the Create constructor of a TOleControl class. Like every other TComponent descendant, it receives an Owner component, but looking at the code reveals that it also calls the InitControlData method, which we've noticed in the generated TWebBrowser code. The internal CreateInstance method of TOleControl is later called from the constructor, and this function uses some of the fields of control data structure filled by InitControlData to instantiate the control interface and set the OleObject interface by using the COM library's CoCreateInstance function.
By now, it's clear that a TOleControl-derived class starts a specific ActiveX control by filling the control data with the specific information of this ActiveX control. It provides access to the control-specific functionality (after it was created) via the CreateInterface property.
One would assume that the Delphi IDE knows how to fill the control data memory structure when it creates the control's wrapper code by reading the control's type library and setting all the information as required. In theory, we could implement the code that reads and parses a type library, and reproduce this functionality at run time to create a run-time embeddable control host. My needs for run-time embedding of ActiveX controls are actually rather simple, so instead of going to the trouble of implementing this code, I chose to take a simpler approach.
Different Class IDs, Same Control I decided that I wanted all the controls to be treated as if they were the same control. They would all have the same functionality as far as my application were concerned. The only difference between them would be the implementation details, which aren't of any interest to the application.
Therefore, I decided to create a TOleControl descendant that will have the same ControlInterface (the same functionality as far as the application is concerned), and will only differ in the ClassID between the different ActiveX controls that will be embedded by the component. This doesn't make the class I will discuss here a true Jack-of-all-trades class that can host any ActiveX control, but a more limited control host that can load at run time all the ActiveX controls that implement a specific interface.
If you remember our sample application, the media indexing application we discussed previously, you can see that every media browser object needs to support the same functions: receive a pointer to the media element data, and display this data. For most applications that want a run-time ActiveX hosting facility, a common interface can be easily found. Obviously, a development tool, such as Delphi, that needs to be able to host every kind of ActiveX regardless of interfaces that it supports, will need to be able to read and parse the control's type library, but our application is much simpler.
Defining the Control's Functionality Interface To use the embedded control from the application, we need to define the interface that the control must implement. This is really an application-specific task, but some guidelines can be useful:
Basically, you can think of the control's life cycle as follows: 1) The user performs an operation that requires the activation of control X. 2) The application creates the control using the GUID and embeds it in its window. 3) The application uses the SetApplication method of the interface to tell the control how it can call it (the application) back. 4) The application uses the SetInformation method to transfer some information to the control so it will be able to initialize itself. 5) The application calls the control's Activate method. The control now needs to activate its user interface and be ready to accept user input. 6) The user works with the control until done with the object that required the use of the control, and does something that requires the removal of the control (maybe choosing another object, or choosing a close function). 7) The application calls the control's Deactivate method. 8) The application calls the control's GetInformation to get information that the user edited in the control (if editing is part of the control's functionality).
In addition to these functions, which are usually part of a control interface, every application will define more methods that need to be implemented by embedded controls. It usually makes sense to provide methods in the application's object model that facilitate some of the back-and-forth communication carried between the control and the application. For example, assume that the control reads information from a stream managed by the application; the application's object model will need to provide methods that allow the control to perform operations such as ReadInteger, ReadString, etc.
The Sample Application The sample application will be a simple form of the media search application we just discussed. We'll create an application that allows the user to create folders of hierarchical media information. For every media item - a media item will be brought from a standard file (I didn't want to implement a database to store the data for this sample) - the user can provide different attributes, keywords, and other pieces of information.
Our application will provide the visual tree that will allow the user to choose different media items. It will also allow the user to add new items and to save/load a collection of these items. We won't provide any built-in functionality to edit/view any of the media items in the application; instead, the application will define an architecture that will allow us to add media types at run time and implement a viewer/editor for them as ActiveX controls. Figure 1 shows our application displaying a bitmap item using the RTBMP.Viewer ActiveX control.
Designing the Item Data Structure The application stores the project's information in the Objects array of the treeview nodes. The root node and folders have no object associated with them. Nodes that represent a media item have a TMediaItem object associated with them.
Our application saves the information about a project in one file. Therefore, it needs to have a way to read and store all the information about the media items, even when it doesn't know what kind of information a specific media item allows to edit/store about itself.
The TMediaItem goals are as follows:
The design of the class is rather simple: It stores a string field that points to the media item file, stores a string list that holds the keywords associated with the media item, and holds a memory stream that includes all the attribute information. The class definition is shown in Figure 2.
TMediaItem = class(TComponent) private // Pointer to the media file. FMediaFile : string; // The item "editable" attributes. FMediaAttr : TMemoryStream; // The keywords associated with the item. FKeywords : TStringList; protected function GetAutomationObject: IDispatch; virtual; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure WriteToStream(AStream: TStream); procedure ReadFromStream(AStream: TStream); function MatchKeyword(AKeyword: string): Boolean; property MediaFile: string read FMediaFile write FMediaFile; property MediaAttr: TMemoryStream read FMediaAttr; property Keywords: TStringList read FKeywords; property AutomationObject: IDispatch read GetAutomationObject; end; Figure 2: The TMediaItem class definition.
Let's discuss the public section methods and properties briefly:
The Application Object Model The application exposes its functionality to the ActiveX viewer/editor via an object model. For the purpose of this sample, only two Automation objects are defined. The first, named Application, is simply the wrapper around the application and exposes the Selected property that points to the selected media item. The other is the RTViewMediaItem Automation object that is a wrapper around a TMediaItem instance, which we discussed previously.
In addition to access to the media item's MediaFile and Keywords properties, the Automation object provides a set of methods to read/write information from/to the MediaAttr stream. These include methods such as the read/write functions ReadIntFromStream and WriteStrToStream, and maintenance functions, such as ClearStream, ResetStream, and StreamSize.
When the user clicks on a node in the tree that represents a media item, the ActiveX control that is the viewer/editor of this item will get a reference to RTViewMediaItem that represents this item.
The ActiveX Viewer/Editor Common Interface The application will host ActiveX media item viewers/editors that share a common interface. Every ActiveX control that will be created to view/edit a media type needs to implement this interface. We define this interface as IRTViewObject in the application's type library. Its definition follows:
IRTViewObject = interface(IDispatch) ['{ 9EE4FE00-2A1A-11D3-A5EA-0040053BA735 }'] procedure ActivateEditor; safecall; procedure DeactivateEditor; safecall; procedure SetEditedObject(const AnObj: IRTViewMediaItem); safecall; function Get_TypesCount: Integer; safecall; function Get_TypeItem(i: Integer): WideString; safecall; function Get_ViewerGuid: WideString; safecall; property TypesCount: Integer read Get_TypesCount; property TypeItem[i: Integer]: WideString read Get_TypeItem; property ViewerGuid: WideString read Get_ViewerGuid; end;
Again, let's discuss some of the procedures and properties:
Creating a Run-time Control Host So far, we've figured out that ActiveX control hosts in Delphi descend from TOleControl, and that during the time Delphi imports a control, it parses its type library and builds the knowledge about the control interface into code. We, on the other hand, decided that we will pre-define the control's functionality by defining an interface that the control will have to implement.
Now is the time to implement our run-time ActiveX control host. The rtAXHost.pas unit, available for download with the rest of this article's code, implements the TRuntimeActiveXHost class, a descendant of TOleControl. The definition of the class is shown in Figure 3.
TRuntimeActiveXHost = class(TOleControl) protected FIntf: IRTViewObject; FClassID: TGUID; CControlData: TControlData; procedure InitControlData; override; procedure InitControlInterface(const Obj: IUnknown); override; public constructor CreatePlugin(AOwner: TComponent; AParent: TWinControl; AClassID: TGUID); procedure Activate; procedure Deactivate; procedure SetEditedObject(EditedObj: IRTViewMediaItem); property ControlInterface: IRTViewObject read FIntf; end; Figure 3: The class definition for TRuntimeActiveXHost.
The CreatePlugin constructor is used to instantiate the control. It receives a parent TWinControl to display itself on, and a GUID for the ActiveX class. The control then creates the ActiveX control and displays it in the area of the parent control.
The InitControlData method is used to set the ActiveX type library information. It initializes the CControlData structure that defines the number of events, the class ID, and other ActiveX options. The Activate, Deactivate, and SetEditedObject methods are wrappers around the calls to the ActiveX control's methods that are part of the IRTViewObject we discussed previously.
For most projects where you need to embed ActiveX controls, you can use RTAXHost.pas as your skeleton, and replace the interface-specific methods for your application. The application uses the TRuntimeActiveXHost control when the user clicks on a node in the tree that represents a media item.
The TreeView's OnChange event is defined as follows:
procedure TRTViewer.ItemsViewerChange(Sender: TObject; Node: TTreeNode); begin FreeCurrentViewer; SwitchToNode(Node); end;
First, we release the currently active viewer (if one exists), and then switch to the new node. Switching to a new node uses the following code fragment in the case of a media item:
FCurrentItem := TMediaItem(ANode.Data); CreateActiveXHost; if (Assigned(RTHost)) then RTHost.Activate;
where RTHost is an instance of TRuntimeActiveXHost that was created by calling the CreatePlugin constructor from CreateActiveXHost. In the next section, we'll discuss ActiveX control registration with the application and show how the creation is accomplished. Notice that after the control host has been created, its Activate method is called (which in turn calls the actual ActiveX's Activate method).
ActiveX Control Registration and Activation The application needs to know which ActiveX control to activate for a specific media type. The application keeps an internal string list that contains lists of names and values in the format, as follows:
TYPENAME=GUID
TYPENAME is the extension of the file's media type (for example, BMP for bitmaps, HTML for html, etc.). The GUID is a string representation of the class ID of the ActiveX control that implements the viewer for this media type.
When the CreateActiveXHost method is called to create the viewer, the media item's file extension determines the media type, and the internal list is used to get the GUID of the ActiveX. This GUID is passed to the CreatePlugin method of the ActiveX control host, and the viewer is created.
The internal list is read from the registry upon application startup, but the values have to be added to this list somehow. We can't expect users to type 40-character-long GUID numbers to associate them with specific media types, but we can expect them to type the control name.
Registration of new ActiveX viewers is done using the application's Options | Add ActiveX menu item. The user is asked for the ActiveX name (e.g. RTBMP.Viewer, which we'll discuss later).
The application then starts the ActiveX as if it was a simple Automation object (using CreateOleObject), and uses the properties TypesCount, TypeItem, and ViewerGuid, which the ActiveX needs to implement. An ActiveX viewer can register itself with the system for more than just one media type; the RTBMP.Viewer ActiveX control supports BMP and WMF files, for example.
Application Roundup Believe it or not, this is all we need to do to host ActiveX controls at run time in the application. We will now continue to discuss a sample viewer ActiveX that can display graphic files (*.bmp, *.wmf), and edit keywords and notes associated with this file.
Writing a Bitmap Viewer ActiveX Control We're now ready to start the real fun: writing ActiveX controls that can be registered with the application and provide viewers/attribute editors for the different media types. The sample we'll introduce is a bitmap and WMF file viewer.
We start by creating a new ActiveX library. I named this library RTBMP. Next, we need to create an ActiveX control in this library. The easiest way to create such a control in Delphi is to use the File | New | ActiveForm wizard.
I gave the name Viewer to the ActiveForm (the class name is TViewer) and saved the ActiveForm file under the name BMPVIEW.pas (and BMPVIEW.dfm).
To create an ActiveX that can be used by our application, we must add the application's typelib unit (RTVIEW_TLB.PAS) to the uses clause of the ActiveForm unit, and add the IRTViewObject interface to the list of interfaces supported by the ActiveForm unit. The definition of the TViewer class now reads:
TViewer = class(TActiveForm, IViewer, IRTViewObject)
We must also add the interface methods to the class definition as follows:
procedure ActivateEditor; safecall; procedure DeactivateEditor; safecall; procedure SetEditedObject(const AnObj: IRTViewMediaItem); safecall; function Get_TypesCount: Integer; safecall; function Get_TypeItem(i: Integer): WideString; safecall; function Get_ViewerGuid: WideString; safecall;
Before we continue to inspect the implementation, let's discuss the graphical UI that will be used for the control. We will use a TImage control to display the media file. The top of the window will have a tabbed interface with one tab that includes a keyword list and buttons to add/remove keywords, and the other tab will include a memo where the user can write notes about the picture.
In addition to these, we'll have a checkbox at the bottom of the control that, when clicked, stretches the image to the size of the available space and, when unchecked, will display the image in its original size. We'll also define the property EditedObject of IRTViewMediaItem type that will be tracked by the control to provide access to the application's media item.
Let's start looking at the code for the control registration. The Get_ViewerGuid method returns the GUID created by Delphi for our class in the typelib unit file. (In RTBMP_TLB.pas, we copy the value of Class_Viewer and return it in our method):
function TViewer.Get_ViewerGuid; begin Result := '{ 01B4C0E3-2A36-11D3-A5EA-0040053BA735 }'; end;
We now continue to the task of defining the types that will be associated with the viewer. Delphi's TImage supports BMP and WMF files, so we'll register ourselves with these two types. Get_TypesCount defines the number of types our viewer supports, and Get_TypeItem returns the different types:
function TViewer.Get_TypesCount; begin Result := 2; // BMP and WMF are supported. end;
function TViewer.Get_TypeItem; begin case i of 0 : Result := 'BMP'; 1 : Result := 'WMF'; end; end;
Let's discuss the other functions. SetEditedObject sets the internal EditedObject property to the IRTViewMediaItem, which is passed to us from the application:
procedure TViewer.SetEditedObject; begin FEditedObject := AnObj; end;
ActivateEditor is called when the viewer is activated. We access the EditedObject to get the name of the media file and display it in the TImage control, read the keywords information from the control, and, if the attribute stream associated with the item is not empty, we read the note and the value of the stretch checkbox using the stream functions that the application exposes to us:
procedure TViewer.ActivateEditor; var FileName : string; begin FileName := EditedObject.MediaFileName; Image1.Picture.LoadFromFile(FileName);
KeywordList.Items.Text := EditedObject.Keywords; if (EditedObject.StreamSize > 0) then begin EditedObject.ResetStream; Memo1.Lines.Text := EditedObject.ReadStrFromStream; StretchBox.Checked := EditedObject.ReadBoolFromStream; StretchBoxClick(Self); end; end;
Finally, DeactivateEditor saves the note, keywords, and the value of the stretch checkbox to the memory stream in the application:
procedure TViewer.DeactivateEditor; begin EditedObject.Keywords := KeywordList.Items.Text; EditedObject.ClearStream; EditedObject.WriteStrToStream(Memo1.Lines.Text); EditedObject.WriteBoolToStream(StretchBox.Checked); end;
The rest of the code in the control deals with the trivial issues of managing and editing the keywords.
It's remarkable: Every media item viewer that we'll implement will be as simple to create as this, because of the simple architecture we created.
Using the Application To use the application, you need to perform the following steps: 1) Compile the project RTView.dpr. 2) Compile the ActiveX project RTBMP.dpr, and use the Run | Register ActiveX Server option to register the control with the system. 3) Start the application, and use the Add Folder button to create a folder. 4) Use the Add Item button and add a *.bmp file (you should have some in Delphi's images subdirectory or in the Windows directory). 5) Click on the new Bitmap node, and notice that nothing is displayed. 6) Let's now register the BMP viewer with the application. First, click on the root node in the tree. Select the Options | Add ActiveX... menu option and enter the ActiveX name RTBMP.Viewer (ProjectName.ClassName). Click the OK button. 7) Choose the media file item in the tree again. You should now be able to see the file and the keyword/notes editor in the pane to the right of the image.
While not discussed in this article, the sample code includes the source of another media view project, named RTHTML.Viewer, which can be compiled, registered with the application, and used to view *.html and *.htm files using the MSIE WebBrowser control. Figure 4 shows the RTHTML.Viewer.
Conclusion ActiveX controls can be embedded in Delphi applications at run time with surprisingly little work. Combining the techniques described in this article, and the plug-in article from the December 1999 issue, allows us to take another step into the design of extendable applications that can be updated and enhanced without recompilation.
The use of an extension framework based on COM and ActiveX makes our product extendable by every development tool that supports these Windows technologies. Delphi, Visual Basic, Visual C++, and others can all be used to create plug-ins and visual extensions to the application.
The project files referenced in this article are available for download.
Ron Loewy is a software developer for HyperAct, Inc. He is the lead developer of eAuthor Help, HyperAct's HTML Help authoring tool. For more information about HyperAct and eAuthor Help, contact HyperAct at (515) 987-2910, or visit http://www.hyperact.com.
|
![]() |
|