Prerequisites:
ESRI ArcMap 8.x (ArcGIS Desktop / ArcEditor / or ArcInfo)
Borland Delphi 6 (versions 4 to 6 should work, but the samples are in written in version 6)
A lot of coffee J
How to begin
The first step before you start coding is to install the ArcObjects Controls ActiveX in Delphi.
- Select “Import ActiveX Control” from the “Component” menu in Delphi’s IDE.
- Select “ESRI ArcObjects Controls 8.1 (Version 1.0)” from the list of available ActiveX controls (version info might differ depending on what version of ArcGIS you have installed).
- Click on the “Install” button and select destination package (you might want to create a new package since the resulting units are quite large).
After installing the control the following files should appear in the ‘Imports’ folder under Delphi main folder:
“esriCore_TLB.dcr”
“esriCore_TLB.pas”
“esriControls_TLB.dcr”
“esriControls_TLB.pas”
The first project: An Command implantation
In this simple project we are going to create an ArcMap Command. Commands can be placed both on the various toolbars found in ArcMap or on the menus. In order to use an ActiveX object as a Command it will have to implement the ICommand interface. Objects that are to be loaded by ArcMap will have to reside in an ActiveX Library.
Start this project by closing all open files / projects in Delphi’s IDE. Then create an ActiveX Library by selecting “File/New/Other” from the menu. Select the tab marked ActiveX and then the ActiveX Library icon.
Now save the project. The name of the completed library (DLL) will be the same as your project name. The shell of the library is now complete but in order for it to be useful we must now create one or more COM objects to fill it with. Do the same thing as before but select the COM Object icon instead, when you click OK the following wizard will appear:
In the field “Class Name”, enter the name of the new COM Object (BTW don’t enter a T before the name since Delphi will add one automatically). Leave the instancing and threading model at the default “Multiple Instance” and “Apartment” in order not to make it more difficult than need be. When you type a class name Delphi will automatically suggest an interface implementation based on that name but since this simple COM object should only implement IUnknown and ICommand you should clear that field. Next click on the “List” button and select ICommand (make sure that the type library is “esriCore.olb”). Finally Click “OK”.
You should now have a new unit with a class declaration like this and function stubs:
uses
ComObj, ActiveX, Project1_TLB, StdVcl, esriCore_TLB;
type
TDelphiCommand = class(TAutoObject, ICommand)
protected
function Get_Bitmap(out Bitmap: OLE_HANDLE): HResult; stdcall;
function Get_Category(out categoryName: WideString): HResult; stdcall;
function Get_HelpContextID(out helpID: Integer): HResult; stdcall;
function Get_HelpFile(out HelpFile: WideString): HResult; stdcall;
function Get_Message(out Message: WideString): HResult; stdcall;
function Get_Tooltip(out Tooltip: WideString): HResult; stdcall;
function OnClick: HResult; stdcall;
function OnCreate(const hook: IDispatch): HResult; stdcall;
{ Protected declarations }
end;
Some times Delphi’s import wizard fails to get the entire interface and you might see messages that the time/date has changed for “esriCore_TLB”. The best way to make certain you have the complete interface declared is simply to open the file and copy/paste the interface declaration and write the implementation part yourself (this is also necessary if your COM object implements more than one ArcObjects interface since the wizard only allows you to select one).
Now it’s time to complete the code. First add a private variable that will hold the pointer to the main application object of ArcMap like this:
private
m_pApp : IApplication; //ArcMap application
It’s private because no-one outside the objects needs to access it. Now add a protected variable like this to hold the bitmap for the command:
CommandBitmap : TBitmap; // Glyph for CommandButton
The reason it’s in the protected section is that it has to be accessible to ArcMap.
Next we finish the implementation of the most important function “OnCreate”. It should look something like this:
function TDelphiCommand.OnCreate(const hook: IDispatch): HResult;
begin
Result := S_OK;
try
m_pApp := hook as IApplication;
except
Result := S_FALSE;
end;
end;
As you can see the parameter hook is of the type IDispatch but since we need IApplication we have to cast it as such (as is actually replaced by a call to the IUnknown method “QueryInterface” by the compiler). The return type of all ArcObjects interfaces is always HResult so in order not to have a lot of compiler warning we must give Result a value.
Next comes the implementation of “Get_Enabled“ which is made a bit more complex to demonstrate how to call other methods inside ArcMap.
function TDelphiCommand.Get_Enabled(out Enabled: WordBool): HResult;
var pMxDoc : IMxDocument;
pMap : IMap;
pLayerCount : Integer;
begin
{
Add some logic here to specify in what state the application
should be in for the command to be enabled. In this example,
the command is enabled only when there is at least one data
layer loaded in ArcMap.
m_pApp is set in OnCreate
}
Result := S_FALSE;
try
pMxDoc := m_pApp.Document as IMxDocument;
OleCheck(pMxDoc.Get_FocusMap(pMap));
OleCheck(pMap.Get_LayerCount(pLayerCount));
if pLayerCount > 0 Then
Enabled := True
else
Enabled := False;
Result := S_OK;
except
on E : Exception do
begin
MessageDlg('Exception in DelphiCommand.Get_Enabled: '+E.Message,mtError,[mbOk],0);
Enabled := False;
end;
end;
end;
The OleCheck function is used so that any error returned by the call to ArcMap result in an Exception instead of a HResult code.
The “Get_Bitmap” function is where the bitmap that you see on ArcMap’s buttons is fetched.
In this sample I added the bitmap to the projects “.res” file using Delphi’s image editor.
In this function the bitmap is created and loaded as a resource then handle is then returned to ArcMap, this is why the variable for the bitmap is declared as protected.
function TDelphiCommand.Get_Bitmap(out Bitmap: OLE_HANDLE): HResult;
begin
Result := S_OK;
try
// Create and load bitmap from resource if not already done.
// Warning! This code may result in a slight memory leakage
// if the library is loaded and released multiple times.
if not Assigned(CommandBitmap) then
begin
CommandBitmap := TBitmap.Create;
CommandBitmap.LoadFromResourceName(HInstance,'COMMAND1');
end;
BitMap := CommandBitmap.Handle;
except
Result := S_FALSE;
end;
end;
The next function is self-explaining.
function TDelphiCommand.Get_Category(out categoryName: WideString): HResult;
begin
//Set the category of this command. This determines where the
//command appears in the Commands panel of the Customize dialog.
categoryName := 'myCommands';
Result := S_OK;
end;
For the rest of the implementation see the included sample-code.
Now the our command object is complete and the projects has been built it’s time to include it in ArcMap. First though we have to register our COM object in Windows registry. This is done by selecting “Run/Register ActiveX Server” from Delphi’s menus. After it has been successfully registered it’s time to start ArcMap. Commands are added by using the “Tools/Customize” menu in ArcMap.
Click the “Add from file” button and select ActiveX DLL you just made. The COM objects inside (in this case our command) will be added to the list of available commands. To use it simply drag it from the dialog to a toolbar or a menu.
The second project: An Extension with events
This project is a somewhat more advanced version of the Command project. This time we will make an extension to ArcMap’s Editor and we will also listen to the events fired by it.
We start the project the same way as the first example by creating an ActiveX Library and then a COM object. This time we choose to implement IExtension instead of ICommand but since this object will implement two interfaces and the wizard only allows one we will have to complete it by hand. Open the file “esriCore_TLB.pas“ and copy the interface specifications for IExtension and IEditEvents. The class declaration should now look something like this:
type
TDelphiExtension = class(TAutoObject, IExtension, IEditEvents)
private
FServer : IEditor;
FCookie : Integer;
protected
{ Protected declarations }
{IExtension}
function Get_Name(out extensionName: WideString): HResult; stdcall;
function Startup(var initializationData: OleVariant): HResult; stdcall;
function Shutdown: HResult; stdcall;
{IEditEvents}
function OnSelectionChanged: HResult; stdcall;
function OnCurrentLayerChanged: HResult; stdcall;
function OnCurrentTaskChanged: HResult; stdcall;
function OnSketchModified: HResult; stdcall;
function OnSketchFinished: HResult; stdcall;
function AfterDrawSketch(const pDpy: IDisplay): HResult; stdcall;
function OnStartEditing: HResult; stdcall;
function OnStopEditing(Save: WordBool): HResult; stdcall;
function OnConflictsDetected: HResult; stdcall;
function OnUndo: HResult; stdcall;
function OnRedo: HResult; stdcall;
function OnCreateFeature(const obj: IObject): HResult; stdcall;
function OnChangeFeature(const obj: IObject): HResult; stdcall;
function OnDeleteFeature(const obj: IObject): HResult; stdcall;
end;
As you can see the IExtension interface is very simple and only contains three functions. Startup and Shutdown are executed when ArcMap loads / unloads the library (so be careful of what you try to call on in startup since it might not be loaded at the time). The two private variables FServer and FCookie will be explained later on in the implementation part.
Let’s start with the most important part of the implementation the IExtension functions.
First up is “Get_Name” which simple returns the name of the extension so the code below is enough.
function TDelphiExtension.Get_Name(out extensionName: WideString): HResult;
begin
extensionName := 'DelphiExtension';
Result := S_OK;
end;
Next is the all important “Startup”. In this function we will register ourselves as a implementer of IEditEvents (COM Events works kind of like a mailing list to which you can subscribe if you got the right kind of mailbox (interface) it’s also possible to do this through an intermediate object, what’s known as an event sink although this will not be covered by this primer).
function TDelphiExtension.Startup(var initializationData: OleVariant): HResult;
begin
// initializationData is a variant which is in this case is the Editor object
Result := S_OK;
MessageDlg('TDelphiExtension.Startup',mtInformation,[mbOk],0);
try
// if initializationData is empty we have major problems
if VarIsEmpty(initializationData) then
raise Exception.Create('TDelphiExtension.Startup: initializationData Empty!');
// Cast data as IEditor
FServer := IUnKnown(initializationData) as IEditor;
// Sign up for events from IEditEvents
InterfaceConnect(FServer, IEditEvents, Self as IEditEvents, FCookie);
except
on E : Exception do
begin
MessageDlg('TDelphiExtension.Startup: ' + E.message, mtError, [mbOk], 0);
Result := S_FALSE;
end;
end;
end;
As you can see we sign up for events using the InterfaceConnect function where we ask the Editor (FServer) for events from the interface IEditEvents and sends our own IEditEvents interface as reception point, the FCookie var will hold a unique connection number which is needed when we disconnect.
Finally we have the “Shutdown” part where disconnect from the events and do any additional cleanup.
function TDelphiExtension.Shutdown: HResult;
begin
Result := S_OK;
try
MessageDlg('TDelphiExtension.Shutdown',mtInformation,[mbOk],0);
// If signed up for events, disconnect from IEditEvents
if Assigned(FServer) then
InterfaceDisconnect(FServer, IEditEvents, FCookie);
except
on E : Exception do
begin
MessageDlg('TDelphiExtension.Shutdown: ' + E.message, mtError, [mbOk], 0);
Result := S_FALSE;
end;
end;
end;
The implementation for IEditEvents itself is in this sample is simple since it’s only designed to show the function. It looks like this for all methods:
function TDelphiExtension.OnSelectionChanged: HResult;
begin
MessageDlg('TDelphiExtension.OnSelectionChanged',mtInformation,[mbOk],0);
Result := S_OK;
end;
Finally it’s time for installation of our new extension. After building the library and registering the ActiveX (see first sample) it’s time to make ArcMap take notice of it. This is easiest done using a program “categories.exe” which comes with ArcGIS (usually in C:/arcgis/arcexe81/Bin).
When you run it you will see this dialog.
Find “ESRI Editor Extensions”, click on “Add Object”, point out your new ActiveX library and you’re all done. Now fire up ArcMap and try it out.