How to create a custom tool using AoBaseTool


With ArcGIS Engine, you can write custom tools to add to your applications. Your custom tool will be a button or menu that interacts with the controls when it is selected. Custom tools allow you to easily add custom functionality to your ArcGIS control applications without having to listen for all the events on the controls.
If you only want to have an action performed when the custom toolbar button is clicked (as, for example, a zoom to full extent command), you only need to write a custom command. For further details, see Creating a custom command using AoCommandBase.
Since tools are a GUI tool to interact with ArcGIS controls, C++ custom tools will only work on Solaris and Linux. If you want to write a command or tool to plug into ArcMap or ArcCatalog, you will need to use a different language, such as Visual C++.
To create a new tool, you will use the esriSystemUI ICommand and esriSystemUI ITool interfaces. Through those interfaces you will be able to set the properties and behavior of your tool. Some of the properties that you can set through the ICommand interface are the command's name, bitmap, caption, category, statusbar message, tooltip, enabled state, and checked state. It also defines the action taken when your tool is clicked. Through the ITool interface, you will be able to specify what cursor to use; what to do when the mouse button is pressed; released, or double-clicked; what to do when the mouse is moved; what to do when a key is pressed down or released; and what to do when a screen display in the application is refreshed.
To create custom tools with the C++ API, there is a helper class for ICommand and ITool that you will inherit from: CAoToolBase. This is defined in arcgis/developerkit10.0/include/Ao/AoToolBase.h and includes AoToolbarAddTool, which you will use to place your custom tool on a ToolbarControl.

Tool creation example: Custom Pan tool

This tool will allow you to pan around the current map and can be used in any application using a ToolbarControl with a MapControl or PageLayoutControl buddy.

Creating the Pan class: Implementing ICommand

  1. Create a new file in your text editor: Pan.h.
  2. In Pan.h, create a new class, Pan. Include a public constructor and destructor, as well as a private IHookHelperPtr data member, which will contain the hook object passed to the tool when the tool is created. This member variable will provide access to the ActiveView, FocusMap, and PageLayout of whichever control the tool is associated with: a MapControl, PageLayoutControl, or ToolbarControl. You will need to include ArcSDK.h for the Engine.
[Motif C++]
#ifndef __PAN_H_
  #define __PAN_H_
  // ArcObjects Headers
  // Engine
  #include <ArcSDK.h>
  class Pan
  {
    public:
      Pan();
      ~Pan();
    private:
      IHookHelperPtr m_ipHookHelper;
  };
#endif // #define __PAN_H_
  1. Update the Pan class so that it inherits from CAoToolBase. Notice that CAoToolBase inherits from CAoCommandBase. This means you will need to implement both the functions from CAoToolBase and CAoCommandBase. Start with those from CAoCommandBase, placing the function declarations into Pan.h now. Also add a private member variable of type OLE_HANDLE, which you will use to add a bitmap to your custom tool. You will need to include Ao/AoToolBase.h for CAoToolBase and AoToolbarAddTool. You do not need to include Ao/AoCommandBase.h since Ao/AoToolBase.h takes care of that for you. You will also need to include Ao/AoControls.h.
[Motif C++]
#ifndef __PAN_H_
  #define __PAN_H_

  // ArcObjects Headers
  // Engine
  #include <ArcSDK.h>
  // Controls
  #include <Ao/AoControls.h>

  // Custom Tool
  #include <Ao/AoToolBase.h>

  class Pan: public CAoToolBase
  {
    public:
      Pan();
      ~Pan();

      // ICommand
      HRESULT get_Enabled(VARIANT_BOOL *Enabled);
      HRESULT get_Checked(VARIANT_BOOL *Checked);
      HRESULT get_Name(BSTR *Name);
      HRESULT get_Caption(BSTR *Caption);
      HRESULT get_Tooltip(BSTR *Tooltip);
      HRESULT get_Message(BSTR *Message);
      HRESULT get_Bitmap(OLE_HANDLE *bitmapFile);
      HRESULT get_Category(BSTR *categoryName);
      HRESULT OnCreate(IDispatch *hook);
      HRESULT OnClick();

    private:
      IHookHelperPtr m_ipHookHelper;
      OLE_HANDLE m_hBitmap;
  };

#endif // #define __PAN_H_
The code shown in gray has already been entered in previous steps. It is given here to illustrate the accurate placement of the code you are adding in this step.

Adding code to the members of ICommand

  1. Create a new file in your text editor: Pan.cpp.
  2. Include Pan.h.
[Motif C++]
#include "Pan.h"
  1. Implement the constructor after the include directive. Here you will load in the bitmap for the button. You do not need to initialize m_ipHookHelper since it is a smart pointer. Smart pointers are initialized by default to 0. If this were a standard interface pointer, it would have to be initialized in the constructor's initialization list.
[Motif C++]
Pan::Pan()
{
  // Load the bitmap
  IRasterPicturePtr ipRastPict(CLSID_BasicRasterPicture);
  IPicturePtr ipPict;
  HRESULT hr = ipRastPict->LoadPicture(CComBSTR(L "Pan.bmp"), &ipPict);
  if (SUCCEEDED(hr))
  {
    OLE_HANDLE hBitmap;
    hr = ipPict->get_Handle(&hBitmap);
    if (SUCCEEDED(hr))
      m_hBitmap = hBitmap;
  }
}
  1. Next, implement the class destructor, where you will release the member variables.
[Motif C++]
Pan::~Pan()
{
  m_ipHookHelper = 0;
  m_hBitmap = 0;
}
  1. In the following steps you will implement the ICommand functions.
    1. For the tool to be enabled, there needs to be a map for it to work with.
[Motif C++]
HRESULT Pan::get_Enabled(VARIANT_BOOL *Enabled)
{
  if (!Enabled)
    return E_POINTER;
  IMapPtr ipMap;
  m_ipHookHelper->get_FocusMap(&ipMap);
  if (ipMap == 0)
    return S_OK;
  *Enabled = VARIANT_TRUE; // Enable the code always
  return S_OK;
}
    1. Most of the following functions involve simply setting a parameter. Notice the use of VARIANT_TRUE and API calls to create the strings.
[Motif C++]
HRESULT Pan::get_Checked(VARIANT_BOOL *Checked)
{
  if (Checked == NULL)
    return E_POINTER;
  *Checked = VARIANT_FALSE;
  return S_OK;
}

HRESULT Pan::get_Name(BSTR *Name)
{
  if (!Name)
    return E_POINTER;
  *Name = ::AoAllocBSTR(L "Pan");
  return S_OK;
}

HRESULT Pan::get_Caption(BSTR *Caption)
{
  if (!Caption)
    return E_POINTER;
  *Caption = ::AoAllocBSTR(L "Pan");
  return S_OK;
}

HRESULT Pan::get_Tooltip(BSTR *Tooltip)
{
  if (!Tooltip)
    return E_POINTER;
  *Tooltip = ::AoAllocBSTR(L "Pan by Grab");
  return S_OK;
}

HRESULT Pan::get_Message(BSTR *Message)
{
  if (!Message)
    return E_POINTER;
  *Message = ::AoAllocBSTR(L "Pan the display by grabbing");
  return S_OK;
}

HRESULT Pan::get_Category(BSTR *categoryName)
{
  if (!categoryName)
    return E_POINTER;
  *categoryName = ::AoAllocBSTR(L "Developer Samples");
  return S_OK;
}
    1. Since you already have the OLE_HANDLE for your bitmap, setting the tool's bitmap is a simple assignment.
[Motif C++]
HRESULT Pan::get_Bitmap(OLE_HANDLE *bitmap)
{
  if (!bitmap)
    return E_POINTER;
  if (m_hBitmap != 0)
  {
    *bitmap = m_hBitmap;
    return S_OK;
  }
  return E_FAIL;
}
    1. The OnCreate function is passed a pointer to the IDispatch interface of the object. Using the QueryInterface support of the smart pointer, it is a simple matter to set the member variable to be the hook. The smart pointer handles the QI.
[Motif C++]
HRESULT Pan::OnCreate(IDispatch *hook)
{
  m_ipHookHelper.CreateInstance(CLSID_HookHelper);
  if (!hook)
    return E_POINTER;
  m_ipHookHelper->putref_Hook(hook);
  return S_OK;
}
    1. The OnClick method does not need to do anything. The actions will be set based on interactions with the active view when you implement ITool.
[Motif C++]
HRESULT Pan::OnClick()
{
  return S_OK;
}

Creating the Pan class: Implementing ITool

  1. Update Pan.h to include the ITool functions after those for ICommand.
[Motif C++]
// ITool
HRESULT get_Cursor(OLE_HANDLE *cursorName);
HRESULT OnMouseDown(LONG Button, LONG Shift, LONG X, LONG Y);
HRESULT OnMouseMove(LONG Button, LONG Shift, LONG X, LONG Y);
HRESULT OnMouseUp(LONG Button, LONG Shift, LONG X, LONG Y);
HRESULT OnDblClick();
HRESULT OnKeyDown(LONG keyCode, LONG Shift);
HRESULT OnKeyUp(LONG keyCode, LONG Shift);
HRESULT OnContextMenu(LONG X, LONG Y, VARIANT_BOOL *handled);
HRESULT Refresh(OLE_HANDLE ole);
HRESULT Deactivate(VARIANT_BOOL *complete);
  1. To the private section of the class, add member variables to track the cursor, the cursor while panning is taking place, the pan's starting point, and whether the tool is currently in use.
[Motif C++]
OLE_HANDLE m_hCursor;
OLE_HANDLE m_hCursorMove;
IPointPtr m_ipPoint;
bool m_bInUse;
  1. Update the class constructor to set the two cursors after the code for loading the bitmap.
[Motif C++]
// Load the cursors
ISystemMouseCursorPtr ipSysMouseCur(CLSID_SystemMouseCursor);
ipSysMouseCur->LoadFromFile(CComBSTR(L "Hand.cur"));
OLE_HANDLE hTmp;
hr = ipSysMouseCur->get_Cursor(&hTmp);
if (SUCCEEDED(hr))
  m_hCursor = hTmp;
ipSysMouseCur->LoadFromFile(CComBSTR(L "MoveHand.cur"));
hr = ipSysMouseCur->get_Cursor(&hTmp);
if (SUCCEEDED(hr))
  m_hCursorMove = hTmp;
  1. Update the class destructor so that all the new member variables are also released.
[Motif C++]
Pan::~Pan()
{
  m_ipHookHelper = 0;
  m_hBitmap = 0;
  m_hCursor = 0;
  m_hCursorMove = 0;
  m_ipPoint = 0;
}
  1. In the following steps you will implement the ICommand functions.
    1. Although every function must be stubbed out, not all of them need to be implemented. For example, in the functions below you simply return E_NOTIMPL.
[Motif C++]
HRESULT Pan::OnDblClick()
{
  return E_NOTIMPL;
}

HRESULT Pan::OnKeyDown(LONG keyCode, LONG Shift)
{
  return E_NOTIMPL;
}

HRESULT Pan::OnKeyUp(LONG keyCode, LONG Shift)
{
  return E_NOTIMPL;
}

HRESULT Pan::OnContextMenu(LONG X, LONG Y, VARIANT_BOOL *handled)
{
  return E_NOTIMPL;
}

HRESULT Pan::Refresh(OLE_HANDLE ole)
{
  return E_NOTIMPL;
}
    1. Some functions involve simply setting a parameter. Notice the use of VARIANT_TRUE.
[Motif C++]
HRESULT Pan::Deactivate(VARIANT_BOOL *complete)
{
  if (!complete)
    return E_POINTER;
  *complete = VARIANT_TRUE;
  return S_OK;
}
    1. When the pan tool is selected, show the hand cursor. When it is in use, show the hand grabbing cursor.
[Motif C++]
HRESULT Pan::get_Cursor(OLE_HANDLE *cursorName)
{
  if (cursorName == NULL)
    return E_POINTER;
  if (m_hCursor != 0 && !m_bInUse)
  {
    *cursorName = m_hCursor;
    return S_OK;
  }
  else if (m_hCursorMove != 0 && m_bInUse)
  {
    *cursorName = m_hCursorMove;
    return S_OK;
  }
  return E_FAIL;
}
    1. When the mouse button is pushed down, get ready to pan.
[Motif C++]
HRESULT Pan::OnMouseDown(LONG Button, LONG Shift, LONG X, LONG Y)
{
  IActiveViewPtr ipActiveView;
  m_ipHookHelper->get_ActiveView(&ipActiveView);
  if (ipActiveView == 0)
    return S_OK;

  // If the active view is a page layout
  IPageLayoutPtr ipPageLayout(ipActiveView);
  if (ipPageLayout != 0)
  {
    // Create a point in map coordinates
    IScreenDisplayPtr ipPLScreenDisp;
    ipActiveView->get_ScreenDisplay(&ipPLScreenDisp);
    IDisplayTransformationPtr ipPLDispTrans;
    ipPLScreenDisp->get_DisplayTransformation(&ipPLDispTrans);
    IPointPtr ipPLPoint;
    ipPLDispTrans->ToMapPoint(X, Y, &ipPLPoint);
    // Get the map if the point is within a data frame
    IMapPtr ipMap;
    ipActiveView->HitTestMap(ipPLPoint, &ipMap);
    if (ipMap == 0)
      return S_OK;
    // Set the map to be the page layout's focus map
    IMapPtr ipFocusMap;
    m_ipHookHelper->get_FocusMap(&ipFocusMap);
    if (ipMap != ipFocusMap)
    {
      ipActiveView->putref_FocusMap(ipMap);
      ipActiveView->PartialRefresh(esriViewGraphics, NULL, NULL);
    }
  }

  // Create a point in map coordinates
  IMapPtr ipMap;
  m_ipHookHelper->get_FocusMap(&ipMap);
  IActiveViewPtr ipAVFocusMap(ipMap);
  IScreenDisplayPtr ipScreenDisp;
  ipAVFocusMap->get_ScreenDisplay(&ipScreenDisp);
  IDisplayTransformationPtr ipDispTrans;
  ipScreenDisp->get_DisplayTransformation(&ipDispTrans);
  ipDispTrans->ToMapPoint(X, Y, &m_ipPoint);
  ipScreenDisp->PanStart(m_ipPoint);

  m_bInUse = true;

  return S_OK;
}
    1. When the tool is active and the mouse is moved, the visible extent needs to be updated accordingly.
[Motif C++]
HRESULT Pan::OnMouseMove(LONG Button, LONG Shift, LONG X, LONG Y)
{
  if (!m_bInUse)
    return S_OK;
  // Get the focus map
  IMapPtr ipMap;
  m_ipHookHelper->get_FocusMap(&ipMap);
  IActiveViewPtr ipActiveView(ipMap);
  // Move the pan
  IScreenDisplayPtr ipScreenDisp;
  ipActiveView->get_ScreenDisplay(&ipScreenDisp);
  IDisplayTransformationPtr ipDispTrans;
  ipScreenDisp->get_DisplayTransformation(&ipDispTrans);
  IPointPtr ipMoveTo;
  ipDispTrans->ToMapPoint(X, Y, &ipMoveTo);
  ipScreenDisp->PanMoveTo(ipMoveTo);
  return S_OK;
}
    1. When the mouse button is released, stop panning and make sure the display is up to date.
[Motif C++]
HRESULT Pan::OnMouseUp(LONG Button, LONG Shift, LONG X, LONG Y)
{
  if (!m_bInUse)
    return S_OK;
  // Get the focus map
  IMapPtr ipMap;
  m_ipHookHelper->get_FocusMap(&ipMap);
  IActiveViewPtr ipActiveView(ipMap);
  // Stop the pan
  IScreenDisplayPtr ipScreenDisp;
  ipActiveView->get_ScreenDisplay(&ipScreenDisp);
  IEnvelopePtr ipEnv;
  ipScreenDisp->PanStop(&ipEnv);
  // Set the new extent
  ipActiveView->put_Extent(ipEnv);
  // Refresh the active view
  ipActiveView->Refresh();
  m_bInUse = false;
  return S_OK;
}

Using the tool with the ToolbarControl

Your next step is to programmatically place your custom pan tool on a toolbar. For the point of this walkthrough, it is assumed that you have a working application with a ToolbarControl and either a MapControl or a PageLayoutControl. If you do not have one, you can use one of the sample applications. In particular, try MapTocToolbar or PageLayout. To use this sample, copy its files (either the Motif or GTK version) to your folder with the custom pan tool. For this walkthrough, use the MapTocToolbar sample as the base application.

Creating an instance of your tool
  1. Include the header file for your tool in the header file for the control application, in this case MapTocToolbar.h.
[Motif C++]
#include "Pan.h"
  1. Create a global variable of your class type. Since it will need to be deleted when the application is closed, making it a global variable will make that easier to do.
[Motif C++]
Pan *g_pan;
  1. Create a new instance of your class. You will do this right before adding it to the toolbar. For the MapTocToolbar application, this would be done in AddToolbarItems, and that is where the surrounding grayed out code is from.
[Motif C++]
void AddToolbarItems()
{
  ... varTool = L "esriControlCommands.ControlsSelectTool";
  g_ipToolbarControl->AddItem(varTool, 0,  - 1, VARIANT_FALSE, 0,
    esriCommandStyleIconOnly, &itemIndex);
  g_pan = new Pan();
}
Placing your tool on the ToolbarControl
With the help of AoToolbarAddTool, this becomes a simple step of providing which ToolbarControl to add the tool to, the instance of the tool, and the display style for the tool. Since you have provided a button, in this example it will be placed on the ToolbarControl with only an icon. It is added to g_ipToolbarControl because that is the ToolbarControl in MapTocToolbar.
[Motif C++]
void AddToolbarItems()
{
  ... g_pan = new Pan();
  AoToolbarAddTool(g_ipToolbarControl, g_pan, esriCommandStyleIconOnly);
}

Trying it out

Start with the appropriate makefile provided with the MapTocToolbar sample: either Makefile.SolarisMotif, Makefile.LinuxMotif, Makefile.SolarisGTK, or Makefile.LinuxGTK. You will need to update the makefile to get this command to work. First add Pan.cpp as a source:
CXXSOURCES = MapTocToolbar.cpp Pan.cpp
then add it to the dependencies list for your application, and write its dependencies list:
MapTocToolbar.o: MapTocToolbar.cpp MapTocToolbar.h Pan.h
     $(CXX) $(CXXFLAGS) -c -o MapTocToolbar.o MapTocToolbar.cpp
Pan.o: Pan.cpp Pan.h
     $(CXX) $(CXXFLAGS) -c -o Pan.o Pan.cpp
Now that your makefile is set, compile and run your application. Your custom tool will be on the toolbar: select it and pan around the map.