You are on page 1of 25

Programming with ArcObjects 10.

1 in
Delphi XE2
Written by Roger Dunn, Senior GIS Programmer/Analyst for Orem, Utah

July 2012

Introduction

Audience
This document explains how to harness the power of ArcObjects within the Delphi XE2
environment. I use the Win32/64 flavor of Delphi, not the .NET version, so this document is aimed at
native Windows Desktop application developers (Is .NET native? I don't know). Also, I have not
programmed any stand-alone applications with ArcObjects as that requires a separate license granted
either by ArcEngine, ArcGIS Run-time, or EDN. This document is intended to help those who want to
develop extensions or custom tools within ArcMap or ArcCatalog.

Prerequisites
You must have the latest ArcGIS Desktop software installed, as well as the latest Delphi XE2
installed. As of this writing, ArcGIS 10.1 Service Pack 1 and Delphi XE2 Update 4 are the latest versions
of those products. However, many of the principles and practices described in this document work with
previous versions of Delphi and previous versions ArcObjects. I used to maintain a separate document
for each version of Delphi, as each version had its own nuances. That became too big a chore, so I am
now going to maintain this document. Previous documentation for programming ArcObjects 9.3.1 in
Delphi 7, 2005, 2006, 2007, and 2009 can be found at http://arcscripts.esri.com/details.asp?dbid=14204
.

I also recommend bookmarking the ArcObjects help on-line, which can be tricky to find. The
location is http://resources.arcgis.com/en/help/arcobjects-net/conceptualhelp . I'll be referring to this
web site sometimes as the ArcObjects Help. As you read the help, remember that with 32-bit Delphi,
you are developing a COM extension, not an Add-In.

Another very important reference is the API site at


http://resources.arcgis.com/en/help/arcobjects-net/componenthelp/index.html . I'll reference this web
site the ArcObjects API.

Reading Instructions
If you normally read instructions, then you are reading this paragraph. If you skim over
instructions looking for what you need, then you probably missed this paragraph. I want to make a
disclaimer that these are instructions intended to be read by humans who use a computer and are
experienced at doing so. Whenever I use double-quotes around some text, I'm merely naming a lengthy
menu item or file name. The double-quotes help set that text apart from the rest of the sentence and
are not intended to actually BE in the menu item or file name. You should know this by now. For
instance, if I say to go to Tools > Options > Tool Palette and to check on the "Persistent search filter", I
don't actually mean that the Persistent search filter actually has double-quotes around it in the dialog
box.

I am also well aware that there is more than one way to access a given dialog box or a given
function in the Delphi IDE or ArcGIS. Therefore, when I say to right-click something and choose a
particular context menu item, don't complain to me that the stated menu item is also available from the
main menu, or a toolbar, or a keyboard shortcut, and that your way is easier. If I list an instruction to
right-click a project and select Options, that's the same as activating a project (if it's not already) and
then going to Project > Options in the main menu. I know this and don't need to be preached at. I am
also aware of keyboard shortcuts. But since these are customizable for each user, it would be unclear to
have an instruction to press Alt+F7 without an explanation of what I'm assuming that does. I, for one,
use the Visual Studio library of shortcuts, so I run programs with F5, not F9. Use your knowledge of the
Delphi product coupled with these instructions to do things how you want to do them.

Prepare Delphi
Delphi Type Libraries
COM DLLs such as those used in ArcObjects, need to be imported into Delphi using the Type
Library Importer. This can be done manually using the IDE. Since there are a lot of importable files in
ArcGIS Desktop, I have written two batch files to make this job easier. Their names begin with
ImportArcGISCOM. Use either the 32-bit or 64-bit version, depending on your machine CPU type. The
32- and 64-bit versions of the batch file only differ by paths; the 32-bit version uses the C:\Program Files
directory and the 64-bit version uses the C:\Program Files (x86) directory.

You run the batch file from a command prompt, because you have to specify into which version
of Delphi you are importing. In the batch file script (which you’re free to preview and edit in Notepad if
you wish), the specified Delphi version you enter determines where to find the Type Library Importer
executable and where to put the resulting PAS and DCR files. If you wish to import ArcObjects DLLs to
more than one version of Delphi, you must run the command once for each version.

For example, let’s say I am running on 64-bit Windows 7. I want to import ArcObjects DLLs so I
can program with Delphi XE2. I first open a command prompt. Then I use the cd command to change
the current directory to where the ImportArcGISCOM batch file exists. Then I would type the following
and press Enter:
ImportArcGISCOM64 XE2

I end up with several dozen _TLB.pas files in C:\Users\<MyUsername>\Documents\RAD


Studio\9.0\Imports. But as I said, you can modify the batch file so it puts the imported files elsewhere.

Type Library Setting


There is an important checkbox to click when doing ArcObjects development in Delphi. With no
projects open, go to Tools > Options. In the dialog, navigate to Environment Options > Delphi Options >
Type Library. In the "Safecall function mapping" group box, select "All v-table interfaces." Click OK on
the ensuing message and then click OK on the Options dialog.

FYI: When this option is left at "Only dual interfaces", Delphi doesn't create the best PAS files
when importing type libraries. Being a Delphi programmer, you are used to coding with read/write
properties, procedures that return nothing, and functions that return something. But this option makes
it so that every PAS file created with Delphi from ArcObjects contains nothing but functions, all of which
return an HRESULT. Function that would return, say, an Integer, instead have an output parameter of
type Integer, and return an HRESULT. If you want to set the value of a property, you instead call a
function that has the new value as a function parameter, and that function returns an HRESULT. To
further my point, see figures 1 and 2. Which way do you prefer to program? I don't like programming
like Figure 1; it's unnatural. So that's why I change the default setting so my code looks like Figure 2.

procedure SomeMethod(pPoint: IPoint); procedure SomeMethod(pPoint: IPoint);


var begin
dXValue: Double; if pPoint.X = 0 then
begin begin
pPoint.Get_X(dXValue); ...
if dXValue = 0 then end;
begin end;
...
end;
end;
Figure 1 Figure 2

Package
The imported Delphi files now need to become part of a package in the IDE. To do this, please
follow these instructions:

1. In the Tool Palette, expand "Delphi Projects" and double click "Package." You'll see
Package1.bpl in the Project Manager.
2. Right-click Package1.bpl and select Options…
3. On the left, click Description.
4. In the Description edit box, type a name for your package, such as "ArcGIS Desktop ArcObjects
10.1". The text you type here will show up in Delphi's Package List dialog.
5. When finished, click OK.
6. Right-click the package again and select Add…
7. In the resulting dialog, browse to your Imports directory where you (or the batch file) put the
imported ArcGIS Desktop files.
8. Select all files that begin with "esri" and then click Open.
9. The files will be added to your project and one of them will be open in the editor window.
10. In the Project Manager window, expand "Build Configurations" and double-click the "Release"
node. This will make that build configuration active. There should be no need to ever build this
library in Debug mode, as the library is merely a bunch of method wrappers around ArcObjects.
11. Right-click the package yet again and select Save As… Select an appropriate place and name for
the package source. Personally, I create the following path and save my package sources there:
C:\Users\<MyUsername>\Documents\RAD Studio\9.0\Projects\Packages\<PackageName>\
12. Right-click the package and select Make.
13. Unfortunately, your project might not compile. You'll have to look at the error message to know
what to do. After each change, I suggest saving the file and recompiling. Appendix A of this
document has a list of the errors that came from my compile today. You will also get a ton of
warnings about the usage of the dispinterface keyword. I reported this problem to
Embarcadero as Quality Central (QC) report number 88680. The report was closed with the
resolution being "As Designed" with no comments from the sysop. So, I don't know what the
deal is.
14. When the project compiles successfully, you're free to do a Build if you want to.
15. Once the package is compiled, you can install it to the IDE. Right-click the package name in the
Project Manager window and select Install. Although the resulting dialog shows that many
components were added to the IDE component palette, my experience is that these cannot be
used unless you have an ArcEngine Developer license. As I do not have one, I cannot help with
how to use these.
16. Go to File > Close All and click Yes if asked to save any changes.
17. If you imported the _TLB.pas files to a custom directory that Delphi didn't create, you need to
tell other projects where to find your definitions. To do so to Tools > Options… > Environment
Options > Delphi Options > Library, click the ellipses for the Library Path. Proceed to add the
path where the compiled .dcu's are. Click OK. Then in the Browsing path, add the location of
the .pas files themselves and click OK. Then click OK in the Options dialog.
18. Browse to the folder where the ESRI Type Library files were generated
19. You are now ready to develop applications in Delphi XE2 for ArcGIS Desktop 10.1.

Documentation
Load up the ArcObjects API web site I mentioned earlier. In the left-hand pane you see a
number of folders with names that might look familiar. These are the names of your imported PAS files,
without the initial "esri" and without the trailing "_TLB.pas". Suppose you are using the site to search
for help, and the thing you find that you need is an IMxDocument interface. Look at the very top of the
page and you'll see ArcMapUI italicized and in parentheses. That means you need to add
esriArcMapUI_TLB to your uses clause.
Creating an ArcObjects Project

Sample Project
Clearly, if you are reading this, you have in mind some functionality you would like to add to
ArcGIS Desktop via programming in Delphi. Now that Delphi is prepared, it's time to develop the tool.
But as there are so many different kinds of tools you could develop, I can't go over them. Rather, I will
walk you through the development of a command button which opens a window. The window's simple
purpose is to tally the consonants, vowels, numbers, and punctuation marks of all the layer names in the
map. The window refreshes itself as layer names are changed. Code for the project is included with this
document, but where's the fun in that? How will you learn how such a tool is built from scratch?

Create and Save a New Project


1. In the Delphi IDE, go to File > New > Other…
2. Select Delphi Projects > ActiveX > ActiveX Library and click OK. You are presented with an empty
project and an open Type Library Editor.
3. I highly recommend saving at this point—not because anything might get lost, but so that you
can register the empty library with Windows and so that Delphi manages your project better.
Just trust me on this. So, go to File > Save All.
4. Create a new folder where you save Delphi projects. For this example, the folder name I chose
is AlphaWindow.
5. The first file to save is the RIDL file, so save it as AlphaWindow.ridl in the new folder.
6. The next file to save is the Pascal type library, so save it with the suggested name,
AlphaWindow_TLB.pas.
7. Lastly, it asks for the project's name. Again, save it as AlphaWindow.dproj.

Create a New COM Object


1. In the Delphi IDE, go to File > New > Other…
2. Select Delphi Projects > ActiveX > COM Object and click OK. You are presented with the COM
Object Wizard, that used to crash in Delphi XE.
3. CoClass Name should be the name of your class, without the classic T at the beginning. This is
the button we're creating, so I'll call it ViewAlphaWindow.
4. Description should describe the class. This isn't exactly documentation. Sometimes this text is
all you'll see about the object, so fill it in. I put AlphaWindow.ViewAlphaWindow.
5. Threading Model should be Apartment, so that each running ArcMap has a copy of
AlphaWindow.dll loaded into memory.
6. Instancing should be Multiple Instance, so that if ArcMap creates my button class more than
once, each class is a different instance of the class, rather than a shared instance.
7. By default, the Interface shows IViewAlphaWindow, but this is not correct. We are going to
implement the ICommand interface from ArcGIS Desktop. To change this, you can't just type
ICommand and have the wizard be happy. It will work, but it won't do what you are hoping to
do. Instead, click the ellipses button to the right of the edit box and wait for the list to stop
populating.
8. In the Search box, type ICommand and wait.
9. Wait some more.
10. From the results, highlight the ICommand that comes from the esriSystemUI.olb library and click
OK.
11. By selecting an existing interface, the "Mark interface Oleautomation" checkbox will be checked
and disabled; the "Implement existing interface" checkbox will be checked; and the "Include
type library" checkbox will be checked.
12. Click OK.
13. Unit1 will be produced and added to your project, but it's best to save it so the Type Library
knows where the interfaces are implemented for the object. Save it to the AlphaWindow
directory with the name ViewAlphaWindowImpl.pas.
14. The Project Manager window is updated to show you the addition of esriSystemUI_TLB.pas to
the project. It opens that file in another tab.
15. In the Type Library editor (which has the name AlphaWindow.ridl on its tab), click the
ViewAlphaWindow object in the tree, and then click the Implements tab, you'll see that your
class implements ICommand.
16. Note: You've probably seen a dialog that shows you the changes Delphi is making to your units
when an object implements a new interface. The option to see the changes for each Refresh
can be toggled under Tools > Options… > Environment Options > Delphi Options > Type Library >
Source Refreshing > Display updates.
17. Click File > Save All.

Implementing Interfaces
Rather than insert a dissertation on what COM is, if you don't know, you'll have to research that
on the internet. But what we're trying to do here is tell ArcGIS that we've got an object that implements
(has hard code for) a button. But we can't call our functions whatever we want—we have name our
functions the same way ArcGIS Desktop does, and we do that by implementing interfaces like
ICommand. ArcMap has no idea what we're going to do when the Click procedure is executed, but it
knows we have a Click procedure because we implemented ICommand.

1. In ViewAlphaWindowImpl.pas, we have to put some code in the method stubs. What follows is
the name of each method, followed by the code you need to put in the method body, followed
by a comment or two on what we're doing:
a. Get_Bitmap:
i. Result := 0;
ii. I'll show later how to add a custom bitmap to your button or tool.
b. Get_Caption
i. Result := 'View Alphanumeric Stats';
ii. The Caption is the text of the button in ArcGIS Desktop
c. Get_Category
i. Result := 'Analysis Windows';
ii. The Category string is what you see in ArcGIS Desktop when you are in
Customize mode dialog box, in the Commands tab. Categories are listed on the
left side. In your objects, you can specify existing categories, or new ones of
your own
d. Get_Checked
i. Result := False;
ii. A checked button is depressed button that shows that something is on or active.
For instance, the Edge Snapping command is pressed in when Edge Snapping is
on. This method is executed frequently so it better have quick logic.
e. Get_Enabled
i. Result := True;
ii. An enabled button can be clicked whereas a disabled button cannot. This
method is executed frequently, so the internal logic should be simple.
f. Get_HelpContextID
i. Result := 0;
ii. I have no idea how to implement help, so don't ask me.
g. Get_HelpFile
i. Result := '';
ii. See comment on Get_HelpContextID.
h. Get_Message
i. Result := 'The Alphanumeric statistics window shows you totals for character
types within the names of layers in your map';
ii. This string used to be displayed in the status bar when the user hovered over
your button. In 10.1, it is the body of the tooltip window.
i. Get_Name
i. Result := 'AlphaWindow.ViewAlphaWindow';
ii. This string is not visible to ArcGIS Desktop users, but to you as a programmer
and to other programmers.
j. Get_Tooltip
i. Result := 'View Alphanumeric Statistics Window';
ii. This string used to be what was displayed in the pop-up tooltip window when
the user hovered over your button. In 10.1, it is the bold title of the tooltip
window.
k. OnClick
i. ShowMessage( 'Button Clicked!' );
ii. You have to add Vcl.Dialogs to the uses clause of your implementation section.
l. OnCreate
i. Before you create code here, you need to understand what the OnCreate
procedure is. Your custom object will be "CoCreated" several times before
ArcGIS Desktop is ready to use it. When it IS ready, it will call this OnCreate
method and pass you a handle to the application server in the form of an
IDispatch parameter called Hook. Hook might point to an instance of ArcMap,
ArcCatalog, ArcScene, or other ESRI program. Since this button is only intended
for ArcMap, we want to make sure we grab a reference to ArcMap only.
ii. In the interface section, add esriArcMapUI_TLB to the uses clause.
iii. Make a private section within TViewAlphaWindow.
iv. Add a variable pArcMapApp of type IMxApplication to the private section.
v. In the implementation section's uses clause, add System.SysUtils.
vi. In the OnCreate method, your code will be:
1. Supports( Hook, IMxApplication, pArcMapApp );
2. Save the unit and compile the project. It should compile with blue dots in the method stubs
because we're in debug mode. Also, if you don't like that dialog that comes up and shows you
what unit is being compiled, you can turn that off under Tools > Options… > Environment
Options > Compiling group box > Show compiler progress.

Register Your DLL

Register with Windows


There are at least four ways to register your DLL with Windows. Registering an ActiveX library is
telling Windows where clients can find your server on the hard drive when it wants an instance of your
button.

1. On a command line, you can call regsvr32 followed by the full path to your DLL in double-
quotes. Optionally, you can change directory "cd" to your DLL first and THEN call regsvr32
followed by the simple name of your DLL.
2. Within Delphi you can go to Run > ActiveX Server > Register, which will first compile your project
and then register it.
3. You can make a batch file with the register command in it, then double-click the file to register it
in the future without typing anything and without opening the project in Delphi.
4. Another method I really like is being able to register a DLL by right-clicking on it and selecting
Register. To enable this takes some setting up on Windows. If you're interested, follow these
steps:
a. In Windows Explorer, navigate to the following directory, substituting drives and folder
names as appropriate. You might have to enable the showing of hidden directories:
C:\Users\<Username>\AppData\Roaming\Microsoft\Windows\SendTo
b. Right-click an empty area and select New > Shortcut
c. Browse to the following file: C:\Windows\SysWOW64\regsvr32.exe and click Next.
d. Name the shortcut Register Server, or something like that and click Finish.
e. Copy the shortcut file and paste it into the same directory to get Register Server - Copy.
f. Rename Register Server - Copy to Unregister Server.
g. Right-click the shortcut and go into Properties.
h. In the Shortcut tab, find the Target edit box.
i. At the end of the line, type a space and then -u and click OK.
j. Now you can register and unregister servers by right-clicking them and choosing the
appropriate action.
Register with ArcGIS Desktop
To tell ArcMap (or ArcCatalog) to use your DLL, instead of the hundreds already on your
computer, you have two choices. You can add the file to ArcGIS Desktop in Customize mode, but only if
the DLL contains objects that implement ICommand and/or ITool. Currently ours does, but it won't
later. Also, if you have UAC enabled, you have to run ArcMap as administrator to do it successfully.
More on that in a few paragraphs.

The other option is to use a program provided by ESRI to register your DLL with Desktop. This is
more helpful when building an install for your DLL instead of walking your users through the manual
way. The program's path is C:\Program Files (x86)\Common Files\ArcGIS\bin\ESRIRegAsm.exe. It is a
command-line utility that can take an ActiveX or .NET library and register it with Windows and ArcGIS
Desktop at the same time. If you double-click the program from Windows, you get a dialog that
documents the usage of the tool, although poorly.

The ESRIRegAsm program can register your DLLs using three of the four methods described
above: you can do it manually at the command line, in a batch file, or in a custom Windows shortcut.
Delphi can also do it, but you have to set it up first in the Tools menu. To register the DLL in this sample
walk-through, the command would look like this:

"C:\Program Files (x86)\Common Files\ArcGIS\bin\ESRIRegAsm.exe" "<Path to AlphaWindow


project directory>\Win32\Debug\AlphaWindow.dll" /p:Desktop

You should get a Registration Succeeded message box if everything went fine. However, if you
open ArcMap now, your tool will not be accessible. The reason is that we didn't specify that what the
DLL contained was a button for ArcMap's toolbar. This requires using the /f switch. Meaning, you need
to register the classes in your DLL with component categories. Here's how:

1. Create a new, empty text file in your project folder called AlphaWindowCats.xml.
2. The text inside the XML file should look like this:

<?xml version="1.0"?>
<Categories ver="1">
<Category CATID="X">
<Class CLSID="Y"/>
</Category>
</Categories>

3. Find and run C:\Program Files (x86)\ArcGIS\Desktop10.1\bin\categories.exe.


4. In the "Find Category" edit box, put mx commands and then click Find.
5. You'll come to Esri Mx Commands. In case you didn't know, Mx stands for ArcMap, Gx stands
for ArcCatalog, Sx stands for ArcScene, and Tx stands for ArcToolbox.
6. When you highlight the Esri Mx Commands folder, there is a read-only GUI D displayed near the
bottom of the window. Copy that GUID to the clipboard.
7. In your XML file, put the copied GUID, including the curly braces, in place of the A above (but
keep the double-quotes around it).
8. Back in Delphi, in the Type Library Editor, click the name of your class on the left-hand side.
Look at the Attributes window. Copy the GUID to the clipboard.
9. In your XML file, put the copied GUID, including the curly braces, in place of the B above (but
keep the double-quotes around it).
10. Save and close the XML file
11. At a DOS command line, execute the following command:

"C:\Program Files (x86)\Common Files\ArcGIS\bin\ESRIRegAsm.exe" "<Path to AlphaWindow


project directory>\Win32\Debug\AlphaWindow.dll" /p:Desktop /f:"<Path to AlphaWindow
project directory>\AlphaWindowCat.xml"

Hopefully you get a successful message. What this does is create a file in C:\Program Files
(x86)\Common Files\ArcGIS\Desktop10.1\Configuration\CATID. The name of the file is the UID of the
type library (not just the class) in curly braces, followed by an underscore, followed by the name of the
DLL, with an ecfg extension instead. The creation of this file is important and I'll tell you why.
Registering your file right within ArcMap or this method creates the ECFG file in a Windows protected
directory. That means that if you or your user attempts to register the DLL without administrative
privileges or without UAC turned down, then nothing will happen or you'll get an exception.

Also, once you successfully register your DLL, you can see it in the Component Category
Manager (categories.exe). You must close and restart it as it doesn't have a Refresh button. Go down to
the Esri Mx Commands category, expand it, and you'll see your new class listed alphabetically with the
others.

Preliminary Test
If you start ArcMap now and enter Customize mode you'll find your simple command under the
category we specified for it: Analysis Windows. You'll notice that the tool has an icon, but that's a
default one. You'll notice the name of the button is the caption: View Alphanumeric Stats. If you
highlight the command and press the Description button, you'll see a Tooltip with the button's Caption
and the Message we specified in code. Drag and drop the button anywhere you want on any toolbar.
Notice the icon goes away.

Close the Customize window and hover over the View Alphanumeric Stats button. Instead of
the Caption being at the top of the tooltip, the result of the Get_Tooltip function is at the top. And the
Message is still the body of the Tooltip. Click the button and you should get a Button Clicked! message.

Add a GIS layer to your map, and rename it to 123_ABCD. This is for testing purposes. There
are three alphabetic characters, one punctuation character, and four letters. Save the map into your
Delphi project folder. This is important since sometimes your custom code will crash ArcMap and it's a
pain to recreate the map document.
Specifying an Icon
Icons are great for obvious user interface reasons. ESRI provides a whole bunch of them to use
for free. Follow these instructions to put an icon on your new button:

1. Create, open, or convert a 16x16 bitmap in a graphics program like Paint.


2. Per the documentation, the file must be a bitmap where the upper left pixel of the bitmap is
treated as the transparent color. I use magenta because no one in their right mind would
actually put magenta in an icon.
3. Save the bitmap in your project folder.
4. In your Delphi project, go to Project > Resources and Images.
5. Click Add…, select your bitmap, and click Open.
6. Name the bitmap. I named mine ALPHASTATCMD.
7. Click OK.
8. Notice the bitmap is added to your list of project files.
9. Go to the ViewAlphaWindowImpl.pas file.
10. Add Winapi.Windows to the uses clause of the interface section.
11. In the class declaration, add a private member called pBitmap of type HBITMAP.
12. In the Get_Bitmap function, add the following code:

if pBitmap = 0 then
pBitmap := LoadBitmap( HInstance, 'ALPHASTATCMD' );
Result := pBitmap;

13. Create a public section in TViewAlphaWindow.


14. Override the destructor by declaring "destructor Destroy; override;"
15. Do class completion to have Delphi make an empty method stub and put your cursor there.
16. In the empty line of the method body, put "if pBitmap <> 0 then DeleteObject(pBitmap);"
17. Save All.
18. With ArcMap closed, recompile your project.

Note that you can never recompile/rebuild your project while ArcMap is open and your tool is in it.
That's because Windows creates a lock on your DLL that makes it not be overwritten when a client is
using it.

Using Forms
One tricky thing to get working in ArcObjects is form programming, because they're used in
Delphi somewhat differently than in Visual Basic or .NET, and it's hard to interpret the code from those
environments. Our example will have a floating dockable window that shows, as stated before, the
number of consonants, vowels, numbers, and punctuation marks in all the layer names in the map.

Create the VCL Form


1. Go to File > New > VCL Form – Delphi.
2. There are several ways you could design this form, so I'll leave the details to you. But you want
4 labeled expressions that show integers for Consonants, Vowels, Numbers, and Punctuation
Marks (this will be a catch-all category, like Other). This could be done with TLabels and read-
only TEdits, a TStringGrid, a read-only TMemo,
3. Set the Name property of the form to AlphaStatsForm.
4. Create a private procedure called ShowStats which takes 4 Integers called Consonants, Vowels,
Numbers, and Punctuation and puts those values into the controls on your form.
5. Save the form file as AlphaStatsFormWin.pas.
6. Add System.Character to the implementation section's uses clause.
7. Create a private procedure of the form called Calculate, which takes a TStrings object called
LayerNames and has four out parameters of type Integer. The implementation should be like
this

procedure TTAlphaStatsForm.Calculate(StringList: TStrings; out


Consonants,
Vowels, Numbers, Punctuation: Integer);
var
Line, CharIdx: Integer;
A: Char;
begin
Consonants := 0;
Vowels := 0;
Numbers := 0;
Punctuation := 0;
if StringList <> nil then
for Line := 0 to StringList.Count - 1 do
for CharIdx := 1 to Length( StringList[ Line ] ) do
begin
A := StringList[ Line ][ CharIdx ];
if TCharacter.IsLetter( A ) then
if CharInSet( TCharacter.ToUpper( A ), [ 'A', 'E',
'I', 'O', 'U' ] ) then
Inc( Vowels )
else
Inc( Consonants )
else
if TCharacter.IsNumber( A ) then
Inc( Numbers )
else
Inc( Punctuation );
end;
end;

8. Create a public procedure called CalcAndShowStats which takes a TStrings object called
CalcAndShowStats and has the following code:
procedure TTAlphaStatsForm.CalcAndShowStats(LayerNames:
TStrings);
var
C, V, N, P: Integer;
begin
Calculate( LayerNames, C, V, N, P );
ShowStats( C, V, N, P );
end;

9. By putting these pieces of logic into three different methods, testing becomes easier. You could
use Delphi's DUnit testing suite, making the private functions public, and testing the results of
various inputs without ever opening ArcMap. None of the three methods described here uses
any ArcObjects. It uses Delphi controls, system units, and data structures independent of GIS.
The DUnit testing suite would create the form as TAlphaStatsForm instead a COM object. You
could even take out the code that doesn't deal with controls and separate it.
10. Save All.

Create the Wrap-Around COM Object


Researching the ArcObjects documentation reveals that we have to implement
IDockableWindowDef. We can't make a form and just have it implement the necessary interfaces. We
have to make a wrap-around object which creates and uses the form we created in the project. So, we
create the outer object like before. Follow these steps:

11. In Delphi go to > File > New > Other…


12. Select Delphi Projects > ActiveX > COM Object
13. For CoClass, put AlphaStatWindow. Remember not to put a "T" there as this is not the name of
your Delphi class, but your COM class. But don't be dumb. If your COM class starts with a T, as
in ToolboxOrganizer, then of course begin the name with a "T."
14. For Description, put AlphaWindow.AlphaStatWindow. Or, you could put something more
descriptive. I just don't see this property used much, but that could be just my perception.
15. Click the ellipses button to the right of Interface. We are not going to implement something
called IAlphaStatWindow. Note, however, that you can if you want to, but then you have to
make up some methods and properties for this new interface.
16. Once the Interface Selection Wizard populates, and you've clipped your nails, search for
IDockableWindowDef. It's in the esriFramework.olb type library.
17. Click OK.
18. You'll notice that esriFramework_TLB gets created and added to your project. Note also that
this isn't the one generated by the type library importer by the batch file. Delphi does this
automatically. In the Type Library Editor, you'll notice that the new object is there.
19. Flip to Unit1 and save it as AlphaStatWindowImpl.pas.
20. Do you like the suffix "impl"? It rhymes with pimple.
21. Populate the following method stubs:
a. Get_Caption
i. Result := AlphaStatsForm.Caption;
ii. Even if we decided to return something silly, we wouldn't see this text. The
caption of the window is what we've set in the form's Caption property.
b. Get_ChildHWND
i. Result := AlphaStatsForm.Handle;
ii. Very important!
c. Get_Name
i. Result := 'AlphaWindow.AlphaStatsWindow';
ii. No idea.
d. Get_UserData
i. Result := 'ABC';
ii. Per documentation, this is custom to the window.
e. OnCreate

procedure TAlphaStatWindow.OnCreate(const hook: IDispatch);


var
pAppl: IApplication;
begin
Supports( hook, IMxApplication, pArcMapApp );
if not Assigned( AlphaStatsForm ) then
begin
if Supports( pArcMapApp, IApplication, pAppl ) then
AlphaStatsForm := TAlphaStatsForm.CreateParented(
pAppl.hWnd );
if Assigned( AlphaStatsForm ) then
AlphaStatsForm.Show;
end;
end;

i. Like the OnCreate method of the button, there is some work to do here. And
remember that this isn't when the object gets created—it's when ArcGIS says
it's done creating it.
ii. In the interface section's uses clause, add esriArcMapUI_TLB.
iii. Add a private member called pArcMapApp of type IMxApplication.
iv. In the implementation section's uses class, add System.SysUtils.
v. Notice how we create the form. We don't use the normal Create constructor
because the parent is not a VCL object. But it's not enough to call
CreateParented with a valid handle either. If you don't call Show afterwards,
then ArcMap will show a blank form with no controls on it. It's also interesting
to note that ArcMap remembers which dockable windows were visible and
which weren't when it last closed. If your dockable window was closed last
time, the call to Show won't force it to be visible—ArcMap will control that.
f. OnDestroy
i. FreeAndNil( AlphaStatsForm );
ii. The reason I don't just Free the form is because I also want to set the
AlphaStstForm variable to nil after I free it. In this DLL, the window COM object
is being created in the multi-instance, apartment mode (see the call to
TAutoObjectFactory.Create at the bottom of the unit). We only want to create
one window, and have one pointer to it, and free it once.
22. Save All.

Call the Form from the COM Object


1. Now to connect the TForm to our COM object. Go to the AlphaStatWindowImpl unit. Add
AlphaStatsFormWin to the implementation section's uses clause.
2. Now open ViewAlphaWindowImpl.pas and modify some of the methods and the class itself.
Instead of having the button always enabled and always unchecked and show a simple dialog on
click, we're going to do some real work.
3. Add a private variable to the TViewAlphaWindow class called pAlphaWin of type
IDockableWindow;
4. For IDockableWindow to be defined, add esriFramework_TLB to the uses clause of your
interface section.
5. Create a private procedure called FindAlphaWin and do class completion to get an empty
method stub. The body should look like this:

procedure TViewAlphaWindow.FindAlphaWin;
var
pDocWinMgr: IDockableWindowManager;
pWinID: IUID;
begin
if pAlphaWin = nil then
if Supports( pArcMapApp, IDockableWindowManager,
pDocWinMgr ) then
begin
pWinID := CoUID.Create;
pWinID.Value := GUIDToString( CLASS_AlphaStatWindow );
pAlphaWin := pDocWinMgr.GetDockableWindow( pWinID );
end;
end;

6. Change the following existing implementations:


function TViewAlphaWindow.Get_Checked: WordBool;
begin
FindAlphaWin;
Result := ( pAlphaWin <> nil ) and pAlphaWin.IsVisible;
end;

function TViewAlphaWindow.Get_Enabled: WordBool;


begin
FindAlphaWin;
Result := pAlphaWin <> nil;
end;

procedure TViewAlphaWindow.OnClick;
begin
FindAlphaWin;
if pAlphaWin <> nil then
pAlphaWin.Show( not pAlphaWin.IsVisible )
end;

7. IUID is an interface declared in the esriSystem_TLB unit, so add that to the uses clause of the
implementation section.
8. Since the OnClick procedure isn't showing a message dialog, you can remove Dialogs from the
uses clause of the implementation section.

If you try and compile the project at this point, you'll get a mysterious error: E2037 Declaration
of 'Get_Bitmap' differs from previous declaration. Yet, you didn't change anything related to that
function. This is a classic example of identifier resolution that happens in Delphi. The problem lies in
the fact that OLE_HANDLE is defined in two different ways in units that your project uses.

If you Ctrl+Click OLE_HANDLE on the line that gives you the error, Delphi will open
esriSystem_TLB. It defines it as an Integer. Now go back to your unit and find the declaration of the
Get_Bitmap function. Ctrl+Click on that OLE_HANDLE. Delphi opens Winapi.ActiveX and shows you that
it is an alias type for THandle. Ctrl+Click again to find THandle is an alias for System.THandle. Ctrl-Click
one last time on System.THandle and you'll find it's an alias for NativeUInt, which is NOT the same as
Integer. Delphi doesn't think the declaration and definition of Get_Bitmap match because it interprets
OLE_HANDLE differently in each section. One solution is to fully qualify OLE_HANDLE in the definition of
Get_Bitmap like this: Winapi.ActiveX.OLE_HANDLE;

When it comes to identifier resolution, the order of units in your uses clause completely comes
into play. Delphi will always use the definition for a type by going through the units backwards as they
are listed in the uses clause. Typically, then, the identifiers you have to fully qualify belong in units that
are towards the front of your uses clause. In our example above, if you put esriSystem_TLB in front of
ActiveX, then that also fixes the problem. The only reason I don't like that, personally, is that
esriSystem_TLB is not needed in the interface section; it's needed in the implementation section.
Whatever works for you.
Debugging Your Project
A few steps need to be taken in each ArcObjects project you create in Delphi to make debugging
possible. Those requirements are suggested here:

1. Go to Project Options > Delphi Compiler > Linking.


2. Choose the Target to be "Debug configuration – All platforms" or the "32-bit Windows platform"
sub-target.
3. Check the "Include remote debug symbols" checkbox to make it True. When you compile your
project, another file will be placed there with the same name as your DLL with an RSM extension
(which stands for remote symbol map I think).
4. The above option bloats your DLL and the RSM is also big. But that's OK because you're
debugging and COM needs to communicate with Delphi. When you build in the Release mode,
the DLL will shrink, the RSM won't go away, but you don't deploy the RSM either.
5. Most Delphi developers are used to starting their debugging by hitting a shortcut such as F9 or
F5. If you don't do the next steps, than you'll get a Delphi error message about a host
application not being defined.
6. Click OK in the Options dialog
7. Go to Run > Parameters…
8. Again choose the appropriate Target.
9. For Host application, browse to, or type, the path to the ArcMap executable (or ArcCatalog, if
that's what you're developing for). It's probably C:\Program Files
(x86)\ArcGIS\Desktop10.1\Bin\ArcMap.exe .
10. If it's useful, you can add a double-quoted map document path as a parameter which makes it
open automatically, instead of you having to pick it from the MRU dialog, MRU menu, or the
Open command.
11. Set breakpoints in the code. In our test project, put a breakpoint in the first line in
FindAlphaWin.
12. Run the project.
13. Notice that your breakpoints go into an invalid mode with white X's in it. At 10.1 I also get a few
errors from ArcMap inside Delphi, but then the breakpoints go red again. If you want, you can
check the box to stop getting warnings from that class and click Next.

Finding the Alphanumeric Stats Window


If you can successfully decompile and run your project, it should open ArcMap and very soon
you'll stop within the FindAlphaWin procedure. This procedure is called often, as it is inside the
Get_Checked and Get_Enabled methods, which are called often as long as ArcMap is idle. If you've ever
used TActionManager in Delphi, it's a lot like having a breakpoint in a TAction's OnUpdate event handler.

Anyway, you'll notice that pAlphaWin never stops being nil. Therefore, the button is disabled
forever. The reason is simple, but takes some analysis. Although we have declared the COM wrapper
and the actual form class for the window, we have not registered it with ArcGIS Desktop. So, we'll do
that similarly to how we did the button.
1. Close ArcMap and return to the Delphi IDE.
2. Open AlphaWindowCats.xml in a text editor.
3. The text inside the XML file should already look something like this:

<?xml version="1.0"?>
<Categories ver="1">
<Category CATID="{B56A7C42-83D4-11D2-A2E9-080009B6F22B}">
<Class CLSID="{980ABA12-3B92-4AAA-8999-8682592A3D7F}"/>
</Category>
</Categories>

4. Add another Category tag under the existing one with a Class subtag.
5. Find and run C:\Program Files (x86)\ArcGIS\Desktop10.1\bin\categories.exe.
6. In the "Find Category" edit box, put "mx dock" and then click Find.
7. You'll come to Esri Mx Dockable Windows.
8. When you highlight the Esri Mx Dockable Windows folder, there is a read-only GUI D displayed
near the bottom of the window. Copy that GUID to the clipboard.
9. In your XML file, put the copied GUID, including the curly braces, in the new CATID attribute
(and keep the double-quotes around it).
10. Back in Delphi, in the Type Library Editor, click the AlphaStatWindow class on the left-hand side.
Look at the Attributes window. Copy the GUID to the clipboard.
11. In your XML file, put the copied GUID, including the curly braces, in the new CLSID attribute (and
keep the double-quotes around it).
12. Your new XML should look something like this, although the CLSID values might be different if
you created your own project. They'll be the same if you copied mine.

<?xml version="1.0"?>
<Categories ver="1">
<Category CATID="{B56A7C42-83D4-11D2-A2E9-080009B6F22B}">
<Class CLSID="{980ABA12-3B92-4AAA-8999-8682592A3D7F}"/>
</Category>
<Category CATID="{117623B5-F9D1-11D3-A67F-0008C7DF97B9}">
<Class CLSID="{8B8EEFAF-C160-4DBA-90F0-EE26FF41D494}"/>
</Category>
</Categories>

13. Save and close the XML file


14. At a DOS command line, execute the following command, subsi:
15. "C:\Program Files (x86)\Common Files\ArcGIS\bin\ESRIRegAsm.exe" "<Path to AlphaWindow
project directory>\Win32\Debug\AlphaWindow.dll" /p:Desktop /f:"<Path to AlphaWindow
project directory>\AlphaWindowCat.xml"
16. You should get a successful message.
17. Run the project again, still with a breakpoint in the FindAlphaWin procedure.
18. This time, pAlphaWin is assigned. Moreover, you can click the Show Alphanumeric Statistics
window. Whenever the window is showing, the button is depressed. You can close the window
by clicking the button OR clicking the X on the window. Either way, the button becomes
unpressed. Also cool is that your custom Delphi form can be docked using ArcGIS Desktop's cool
docking mechanism. And, as I said before, ArcMap will remember the form's visible state when
it closes and will return the window to that state when it's reopened.

Updating the Form When Events Are Fired


One thing our dialog doesn't do yet is actually calculate statistics based on the layer names.
What we need to do is listen to events. There are many events that are relevant to our form. Careful
design will reveal that we need to update our stats whenever the following occur:

 A layer is renamed in the Table of Contents


 A layer is renamed in the Layer Properties dialog box
 A layer is added to the map
 A layer is removed from the map
 A group layer is the object in the above 4 situations, instead of a regular layer
 The map document is closed
 A map document is opened

Examining the ArcObjects documentation shows that we need to have our COM window object
(not the VCL one) implement more interfaces. Luckily, all event-related interfaces have Events at the
end of their name, so it's just a matter of finding the right ones.

Exploring the ArcMapUI namespace reveals the IDocumentEvents interface which has events for
CloseDocument, NewDocument, and OpenDocument. Well, what about layer changes? Well, let's start
with that.

1. In the Type Library Editor, click the AlphaStatWindow object.


2. Click the Implements tab.
3. Right-click an empty area and select Insert Interface.
4. Hm. IDocumentEvents is not in the list. This is easy to fix, but not immediately obvious.
5. First we find out what DLL it's in by using the documentation. We find it's the ArcMapUI
namespace.
6. Cancel out of the dialog.
7. Click the AlphaWindow type library object in the left-hand panel of the Type Library Editor.
8. Click the Uses tab.
9. Right-click an empty area and select Show All Type Libraries. XE had a problem with this
because ArcObjects 10.x libraries have letters in their versions and Delphi couldn't handle it.
10. One quick way to scroll is to put focus on the list and type "esri arcmap" and then find "Esri
ArcMap Object Library 10.1" But you'll see in the version column the text "a.1."
11. Right-click the list and select "Show Selected".
12. Go back to step 1 and follow through step 3. But for step 4, IDocumentEvents is in the list now.
13. Select that interface and click OK.
14. Click "Refresh Implementation" in the toolbar.
15. Flip back over to AlphaStatWindowImpl.pas.
16. Seven new methods were added, along with IDocumentEvents in the class declaration, right
after IDockableWindowDef.

There are a few things that happened behind the scenes that are worth noting. In fact, your
code probably won't compile because of them. First of all, esriArcMapUI_TLB was not added to your
project's list of dependencies. Although that happens when you create a new COM object with the COM
Object Wizard, it doesn't happen when you add an existing interface to an existing object in your
project.

However, esriArcMapUI_TLB was still generated. And, what's more, all of its dependencies were
regenerated and put in your project directory. If you do a Syntax Check on your project, it will fail due to
one or more problems listed in Appendix A. The way to fix this situation is to close the generated units
in the IDE and remove them from the directory. That way, Delphi will use the fixed ones that you
generated at the beginning of this document, which are likely in your Imports directory. Now a Syntax
Check or compile should work correctly.

In the BeforeCloseDocument function, return False because our window will never prevent a
document from closing. This is per the documentation. In the OnContextManu menu, set Handled to
False because we don't handle context menus.

Next, we need a private method that goes through the layers in the document and makes a
string list out of them and then sends it to AlphaStatsForm. Use this code:
procedure TAlphaStatWindow.UpdateStatWindow;
var
pApp: IApplication;
pMxDoc: IMxDocument;
pMaps: IMaps;
slLayerNames: TStringList;
I, J: Integer;
procedure AddName( pLayer: ILayer );
var
pGroup: ICompositeLayer;
iSubLayer: Integer;
begin
slLayerNames.Add( pLayer.Name );
if Supports( pLayer, ICompositeLayer, pGroup ) then
for iSubLayer := 0 to pGroup.Count - 1 do
AddName( pGroup.Layer[ iSubLayer ] );
end;
begin
if Assigned( AlphaStatsForm ) then
if Supports( pArcMapApp, IApplication, pApp ) then
if Supports( pApp.Document, IMxDocument, pMxDoc ) then
begin
slLayerNames := TStringList.Create;
try
pMaps := pMxDoc.Maps;
for I := 0 to pMaps.Count - 1 do
for J := 0 to pMaps.Item[ I ].LayerCount - 1 do
AddName( pMaps.Item[ I ].Layer[ J ] );
AlphaStatsForm.CalcAndShowStats( slLayerNames );
finally
slLayerNames.Free;
end;
end;
end;

17. As an aside, notice that this procedure declares a sub-procedure. This is for recursion if
necessary. If a group layer has a group layer has a group layer, this procedure will take care of
adding all the names.
18. For this to compile, you have to add two units to the uses clause of your implementation
section: System.Classes (for TStringList) and esriCarto_TLB (for IMaps interface).
19. Put the following code in CloseDocument, MapsChanged, NewDocument, MapsChanged, and
OpenDocument:

UpdateStatWindow;

20. You're free to run the program if you want to, but none of the events fire for your document?
Any guesses why? That's right: because you haven't registered your object as a listener to
those events. The code to register a COM object as a listener in Delphi is short, but not trivial.
Keep following along to achieve that.
21. The key procedures we're going to use are InterfaceConnect and InterfaceDisconnect. They
both make use of an integer that serves as a kind of handle.
22. Declare a private variable called iDocEventConn, which in this case stands for Integer,
Document, Event, Connection. Its type is Integer.
23. Three of the parameters to InterfaceConnect are now known:
a. IID is going to the GUID of IDocumentEvents. You won't have to copy and paste it.
You'll use a constant defined in esriArcMapUI_TLB.pas
b. Sink is going to the COM object that we have (are) created, cast to an IUnknown.
c. Connection is going to be iDocEventConn, the variable in our class we just defined.
24. The missing parameter we don't know is Source. We have to use the documentation to figure
out what COM class instance will be firing this event (not a COM interface). In our case, the
COM class that does this is the MxDocument class. But which instance is it? We grab it the
same way we did above: by casting our IMxApplication variable to IApplication, and casting its
Document property to an IMxDocument. So, although we grab a COM interface to the object,
it's not an interface that's firing the event—it's an object that implements that interface.
25. The last question is where to put the InterfaceConnect and InterfaceDisconnect procedures.
That's easy—in the OnCreate and OnDestroy methods of our custom COM object,
TAlphaStatWindow. Those methods now look like this:
procedure TAlphaStatWindow.OnCreate(const hook: IDispatch);
var
pAppl: IApplication;
pMxDoc: iMxDocument;
begin
Supports( hook, IMxApplication, pArcMapApp );
if not Assigned( AlphaStatsForm ) then
begin
if Supports( pArcMapApp, IApplication, pAppl ) then
AlphaStatsForm := TAlphaStatsForm.CreateParented(
pAppl.hWnd );
if Assigned( AlphaStatsForm ) then
AlphaStatsForm.Show;
end;
if Supports( pArcMapApp, IApplication, pAppl ) then
if Supports( pAppl.Document, IMxDocument, pMxDoc ) then
InterfaceConnect( pMxDoc, IID_IDocumentEvents, Self as
IInterface, iDocEventConn );
end;

procedure TAlphaStatWindow.OnDestroy;
var
pAppl: IApplication;
pMxDoc: iMxDocument;
begin
if Supports( pArcMapApp, IApplication, pAppl ) then
if Supports( pAppl.Document, IMxDocument, pMxDoc ) then
InterfaceDisconnect( pMxDoc, IID_IDocumentEvents,
iDocEventConn );
FreeAndNil( AlphaStatsForm );
end;

26. Your project should compile and run. When it does, you'll see your stats form has the following
statistics when you open the document that has the 123_ABCD layer in it:
a. Consonants: 3
b. Vowels: 1
c. Numbers: 3
d. Punctuation: 1
27. Perform other events such as starting a new document or opening another one. Your statistic
window will update. This statistics work even if the map document has two or more data
frames (IMaps). But there are still more events that occur that need to update the
Alphanumeric Statistics Window.
28. More research yields the IActiveViewEvents interface which fires ItemAdded and ItemDeleted.
The trouble is that the currently active view fires this, and this could be the data view, layout
view, or other possible views. So, let's get-er done.
29. IActiveViewEvents is declared in Carto, so in the Type Library Editor, click the AlphaWindow
type library name, click the Uses tab, show all type libraries, and turn on Esri Carto Object
Library 10.1. Then go back to Show Selected.
30. Select the AlphaStatWindow object, click the Implements tab, right-click and insert the
IActiveViewEvents interface. OK.
31. Refresh the implementation.
32. Make the AlphaStatWindowImpl.pas unit active in the IDE editor.
33. Save All.
34. Delete the esri* files generated just now, except those that are explicitly part of your project.
35. Move esriCarto_TLB from the uses clause in the implementation section to the interface
section.
36. Add a call to UpdateStatWindow in the following events: ItemAdded and ItemDeleted. The
documentation says that ContentsChanged is fired with documents opening, but we got that
already.
37. If Delphi can't resolve IDisplay, then add esriDisplay_TLB to the interface uses clause.
38. If esriDrawPhase can't resolve, then add esriSystem_TLB to the same section.
39. Lastly, if IEnvelope can't resolve, then add esriGeometry_TLB also.
40. If you try to compile and Delphi gives you an error that the implementation of
IDockableWindowDef.Get_ChildHWND is missing, then this is another case of identifier
mismatch. Fully qualify OLE_HANDLE with Winapi.ActiveX in the front.
41. Lastly, we have to register our object as a listener to the appropriate event type on the
appropriate objects. Unlike the IDocumentEvents object, where we listen to one object, we
need to listen to various objects in the map document for additions and deletions to the map.
What's more, layers can be added and deleted to a map other than the active map (the active
view), so we need to have multiple listeners.

At this point, I lost interest in writing this document, and decided to post it on-line for you to see
and use. I really have a lot to do so I must abandon this project at this time. The Appendix that follows
was copied and pasted from a previous version of this document.
Appendix A

A list of IDE compiler errors I received when compiling the ArcObjects PAS files into a package.
In most of these cases, the problem was that the generated property differed in exact type from a getter
or setter method. I believe the getter and setter method signatures because that's really what
properties are calling underneath. Therefore, I change the property's type to match that of the method.

[DCC Error] esriGeoDatabase_TLB.pas(9417): E2008 Incompatible


types

Interface: IEnumNetEIDBuilderGEN
Property: EIDs
Solution: I changed PPSafeArray1 to PSafeArray

[DCC Error] esriGeoDatabase_TLB.pas(9445): E2008 Incompatible


types

Interface: IEnumNetEIDBuilder
Property: EIDs
Solution: I comment out the EIDs property since this is a write-only array with two input parameters,
and a void return parameter. Therefore, only the Add procedure is necessary.

[DCC Error] esriCatalogUI_TLB.pas(916): E2008 Incompatible


types

Interface: IGxDialog
Property: StartingLocation
Solution: I changed POleVariant1 to OleVariant

[DCC Error] esriSystemUtility_TLB.pas(827): E2128 INDEX, READ


or WRITE clause expected, but ';' found

Interface: IVB6ReferenceHandler
Property: VBProject
Solution: I added " write _Set_VBProject" after OleVariant and before the semicolon

You might also like