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\
\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); var dXValue: Double; begin pPoint.Get_X(dXValue); if dXValue = 0 then begin ... end; end;
Figure 1
procedure SomeMethod(pPoint: IPoint); begin if pPoint.X = 0 then begin ... end; end;
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\\Documents\RAD Studio\9.0\Projects\Packages\\ 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';
d.
e.
f.
g.
h.
i.
j.
k.
l.
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 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. 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. Get_HelpContextID i. Result := 0; ii. I have no idea how to implement help, so don't ask me. Get_HelpFile i. Result := ''; ii. See comment on Get_HelpContextID. 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. 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. 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. OnClick i. ShowMessage( 'Button Clicked!' ); ii. You have to add Vcl.Dialogs to the uses clause of your implementation section. 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 doublequotes. 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\\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" "\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:
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" "\Win32\Debug\AlphaWindow.dll" /p:Desktop /f:"\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. 14. 15. 16. 17. 18.
Create a public section in TViewAlphaWindow. Override the destructor by declaring "destructor Destroy; override;" Do class completion to have Delphi make an empty method stub and put your cursor there. In the empty line of the method body, put "if pBitmap <> 0 then DeleteObject(pBitmap);" Save All. 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 readonly 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:
4. 5. 6. 7. 8. 9. 10. 11. 12.
Add another Category tag under the existing one with a Class subtag. Find and run C:\Program Files (x86)\ArcGIS\Desktop10.1\bin\categories.exe. In the "Find Category" edit box, put "mx dock" and then click Find. You'll come to Esri Mx Dockable Windows. 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. In your XML file, put the copied GUID, including the curly braces, in the new CATID attribute (and keep the double-quotes around it). 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. In your XML file, put the copied GUID, including the curly braces, in the new CLSID attribute (and keep the double-quotes around it). 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.
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" "\Win32\Debug\AlphaWindow.dll" /p:Desktop /f:"\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. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11.
In the Type Library Editor, click the AlphaStatWindow object. Click the Implements tab. Right-click an empty area and select Insert Interface. Hm. IDocumentEvents is not in the list. This is easy to fix, but not immediately obvious. First we find out what DLL it's in by using the documentation. We find it's the ArcMapUI namespace. Cancel out of the dialog. Click the AlphaWindow type library object in the left-hand panel of the Type Library Editor. Click the Uses tab. 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. 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." Right-click the list and select "Show Selected".
12. 13. 14. 15. 16.
Go back to step 1 and follow through step 3. But for step 4, IDocumentEvents is in the list now. Select that interface and click OK. Click "Refresh Implementation" in the toolbar. Flip back over to AlphaStatWindowImpl.pas. 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
21. 22. 23.
24.
25.
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. 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. Declare a private variable called iDocEventConn, which in this case stands for Integer, Document, Event, Connection. Its type is Integer. 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. 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. 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