ESRI's ArcGIS Desktop product includes the applications ArcCatalog, ArcMap, ArcScene and ArcGlobe. The user interfaces for all four applications can be heavily customized, for example, developers can deploy custom toolbars and buttons.
Below is an example of a custom toolbar for ArcMap. The toolbar is developed as a series of COM classes (or .NET classes with interop). The classes must use interfaces defined in the ArcGIS Desktop SDK such as IToolbarDef, IMenuDef, ICommand and IToolControl. For more information on this or other interfaces please visit EDN.
Before (standard implementation)
With ArcGIS Desktop 9.2, development of toolbars was made much easier with the inclusion of base classes in the ArcGIS .NET SDK. However, the basic look and feel of the ArcGIS Desktop user interface has not changed since it was first released in 2000.
In this article, I will describe how to implement a modern UI in an ArcGIS Desktop application using .NET 2.0. This is achieved by hosting a .NET 2.0 ToolStrip inside a traditional ESRI Toolbar. This is the result:
After (using hosted .NET ToolStrip)
With minimal effort it is possible to create an attractive, functional and efficient toolbar. To summarize the benefits of a hosted ToolStrip:
- Office-style mouse-over shading,
- Button icons with an optional alpha channel (see PNG). An alpha channel can be used for anti-aliasing with a background colors so that images appear to be smooth with no sharp edges.
- Button icons can support up to 24bit colors. ESRI buttons only support 4bit colors, that is, only 16 unique colors. With 16 colors, anti-aliasing is practically impossible.
- An entire toolbar need only expose two COM classes, one for the toolbar and one for the hosting component (ToolControl). This reduces the .NET to COM overhead and makes deployment a lot easier, especially for larger toolbars.
- Code consolidation. All UI components can be hosted within a single .NET user control. Less overhead and more code re-used.
- Dynamic toolbar. It is easy to change the toolbar's appearance at runtime. For example, buttons can be added or removed during runtime.
But there is always a catch! :-) The only disadvantage that I have found is that toolbars cannot be resized at runtime.
The code below will create the ArcMap toolbar shown above. To use the code, create a new C# Class Library in Microsoft Visual Studio 2005. In the project properties, tick Register for COM interop.
Please note that the code and methodology used in this post is not endorsed or supported by ESRI. Use this code at your own risk.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Microsoft.Win32;
using ESRI.ArcGIS.Framework;
using ESRI.ArcGIS.SystemUI;
namespace ESRI.ArcGIS.DeveloperSamples.ToolbarNet {
// .NET UserControl that hosts a ToolStrip
[ComVisible(false)]
public class UserControlTest : UserControl{
private enum Action { None, Cut, Copy, Paste, Delete, Zoom25, Zoom50, Zoom100 }
private IApplication m_application = null;
public UserControlTest() {
// Get .NET Bitmaps
Assembly assembly = Assembly.GetAssembly(typeof(Form));
Bitmap bitmap1 = new Bitmap(assembly.GetManifestResourceStream("System.Windows.Forms.cut.bmp"));
Bitmap bitmap2 = new Bitmap(assembly.GetManifestResourceStream("System.Windows.Forms.copy.bmp"));
Bitmap bitmap3 = new Bitmap(assembly.GetManifestResourceStream("System.Windows.Forms.paste.bmp"));
Bitmap bitmap4 = new Bitmap(assembly.GetManifestResourceStream("System.Windows.Forms.delete.bmp"));
bitmap1.MakeTransparent(Color.Fuchsia);
bitmap2.MakeTransparent(Color.Fuchsia);
bitmap3.MakeTransparent(Color.Fuchsia);
bitmap4.MakeTransparent(Color.Fuchsia);
// Create Dropdown Button
ToolStripDropDownButton toolStripDropDownButton = new ToolStripDropDownButton();
toolStripDropDownButton.Text = "Zoom";
toolStripDropDownButton.DropDownItems.Add(new ToolStripMenuItemAction("25%", Action.Zoom25));
toolStripDropDownButton.DropDownItems.Add(new ToolStripMenuItemAction("50%", Action.Zoom50));
toolStripDropDownButton.DropDownItems.Add(new ToolStripMenuItemAction("100%", Action.Zoom100));
toolStripDropDownButton.DropDownItemClicked += new ToolStripItemClickedEventHandler(
this.ToolStrip_DropDownItemClicked);
// Create Toolstrip
ToolStrip toolstrip = new ToolStrip();
toolstrip.GripStyle = ToolStripGripStyle.Hidden;
toolstrip.Items.Add(new ToolStripButtonAction(bitmap1, Action.Cut));
toolstrip.Items.Add(new ToolStripButtonAction(bitmap2, Action.Copy));
toolstrip.Items.Add(new ToolStripButtonAction(bitmap3, Action.Paste));
toolstrip.Items.Add(new ToolStripSeparator());
toolstrip.Items.Add(new ToolStripButtonAction(bitmap4, Action.Delete));
toolstrip.Items.Add(new ToolStripSeparator());
toolstrip.Items.Add(toolStripDropDownButton);
toolstrip.Stretch = true;
toolstrip.MouseEnter += new EventHandler(this.Toolstrip_MouseEnter);
toolstrip.ItemClicked += new ToolStripItemClickedEventHandler(this.Toolstrip_ItemClicked);
// Add Toolbar. Set Size.
this.Controls.Add(toolstrip);
this.Width = 160;
}
public IApplication Application {
set { this.m_application = value; }
}
private void Toolstrip_MouseEnter(object sender, EventArgs e) {
ToolStrip toolStrip = sender as ToolStrip;
if (toolStrip == null) { return; }
toolStrip.Focus();
}
private void ToolStrip_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e) {
ToolStripMenuItemAction toolStripMenuItemAction = e.ClickedItem as ToolStripMenuItemAction;
if (toolStripMenuItemAction == null) { return; }
this.ExecuteAction(toolStripMenuItemAction.Action);
}
private void Toolstrip_ItemClicked(object sender, ToolStripItemClickedEventArgs e) {
ToolStripButtonAction toolStripButtonAction = e.ClickedItem as ToolStripButtonAction;
if (toolStripButtonAction == null) { return; }
this.ExecuteAction(toolStripButtonAction.Action);
}
private void ExecuteAction(Action action) {
switch (action) {
case Action.Cut:
case Action.Copy:
case Action.Paste:
case Action.Delete:
case Action.Zoom25:
case Action.Zoom50:
case Action.Zoom100:
MessageBox.Show(action.ToString());
break;
default:
break;
}
}
// Subclassed ToolStripButton
private class ToolStripButtonAction : ToolStripButton {
private readonly Action m_action = Action.None;
public ToolStripButtonAction(Image image, Action action) : base(image) {
this.m_action = action;
}
public Action Action {
get { return this.m_action; }
}
}
private class ToolStripMenuItemAction : ToolStripMenuItem {
private readonly Action m_action = Action.None;
public ToolStripMenuItemAction(string text, Action action) : base(text) {
this.m_action = action;
}
public Action Action {
get { return this.m_action; }
}
}
}
// The ESRI ArcMap Command that hosts the User Control
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid(ApplicationGuid.GUID_COMMAND_TEST)]
public sealed class CommandHost : EsriCommandControlMx {
public CommandHost() : base() {
this.m_control = new UserControlTest();
}
public override void OnCreate(object hook) {
base.OnCreate(hook);
if (hook != null) {
((UserControlTest)this.m_control).Application = (IApplication)hook;
}
}
}
// The ESRI ArcMap Toolbar
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid(ApplicationGuid.GUID_TOOLBAR_TEST)]
public sealed class ToolbarTest : EsriToolbarMx {
public ToolbarTest() : base() {
this.m_caption = "Test";
this.m_commands.Add(new EsriToolbarCommand(new Guid(ApplicationGuid.GUID_COMMAND_TEST), false));
}
}
// The Abstract ESRI ArcMap Toolbar
[ComVisible(false)]
public abstract class EsriToolbarMx : EsriToolbar {
private const string IMPLEMENT_CATEGORIES = "\\Implemented Categories\\";
[ComVisible(false)]
[ComRegisterFunction()]
private static void RegisterFunction(string regKey) {
Registry.ClassesRoot.CreateSubKey(regKey.Substring(18) + IMPLEMENT_CATEGORIES +
EsriRegistry.COMPONENT_MX_TOOLBAR);
}
[ComVisible(false)]
[ComUnregisterFunction()]
private static void UnregisterFunction(string regKey) {
Registry.ClassesRoot.DeleteSubKey(regKey.Substring(18) + IMPLEMENT_CATEGORIES +
EsriRegistry.COMPONENT_MX_TOOLBAR);
}
}
// Abstract ESRI Toolbar
[ComVisible(false)]
public abstract class EsriToolbar : IToolBarDef {
private const string SUFFIX = "_TOOLBAR";
protected List<EsriToolbarCommand> m_commands = new List<EsriToolbarCommand>();
protected string m_caption = null;
public void GetItemInfo(int pos, IItemDef itemDef) {
itemDef.ID = this.m_commands[pos].Guid.ToString("B");
itemDef.Group = this.m_commands[pos].Group;
}
public string Caption {
get { return this.m_caption; }
}
public string Name {
get { return this.m_caption.ToUpper().Replace(" ", "") + SUFFIX; }
}
public int ItemCount {
get { return this.m_commands.Count; }
}
}
// Abstract ESRI Toolbar Item
[ComVisible(false)]
public class EsriToolbarCommand {
private readonly Guid m_guid = Guid.Empty;
private readonly bool m_group = false;
public EsriToolbarCommand(Guid guid, bool group) {
this.m_guid = guid;
this.m_group = group;
}
public Guid Guid {
get { return this.m_guid; }
}
public bool Group {
get { return this.m_group; }
}
}
// Abstract ESRI ArcMap Command Tool Control
[ComVisible(false)]
public abstract class EsriCommandControlMx : EsriCommandControl {
private const string IMPLEMENT_CATEGORIES = "\\Implemented Categories\\";
[ComVisible(false)]
[ComRegisterFunction()]
private static void RegisterFunction(string regKey) {
Registry.ClassesRoot.CreateSubKey(regKey.Substring(18) + IMPLEMENT_CATEGORIES +
EsriRegistry.COMPONENT_MX_COMMAND);
}
[ComVisible(false)]
[ComUnregisterFunction()]
private static void UnregisterFunction(string regKey) {
Registry.ClassesRoot.DeleteSubKey(regKey.Substring(18) + IMPLEMENT_CATEGORIES +
EsriRegistry.COMPONENT_MX_COMMAND);
}
}
// Abstract ESRI Command Tool Control
[ComVisible(false)]
public abstract class EsriCommandControl : EsriCommand, IToolControl {
protected ICompletionNotify m_completionNotify = null;
protected UserControl m_control = null;
public virtual bool OnDrop(esriCmdBarType barType) {
return true;
}
public virtual void OnFocus(ICompletionNotify complete) {
this.m_completionNotify = complete;
// Can add UI update here
}
public int hWnd {
get { return this.m_control.Handle.ToInt32(); }
}
}
// Abstract ESRI Command
[ComVisible(false)]
public abstract class EsriCommand : ICommand {
protected string m_category = null;
protected string m_message = null;
protected string m_caption = null;
protected string m_tooltip = null;
protected IApplication m_application = null;
protected Bitmap m_bitmap = null;
private IntPtr m_hBitmap = IntPtr.Zero;
[DllImport("gdi32.dll")]
protected static extern bool DeleteObject([In] IntPtr hObject);
protected EsriCommand() { }
~EsriCommand() {
if (this.m_hBitmap != IntPtr.Zero) {
DeleteObject(this.m_hBitmap);
}
this.m_bitmap = null;
this.m_application = null;
}
public virtual void OnClick() { }
public string Message {
get { return this.m_message; }
}
public int Bitmap {
get {
if (this.m_hBitmap == IntPtr.Zero) {
if (this.m_bitmap != null) {
this.m_hBitmap = this.m_bitmap.GetHbitmap();
}
}
return this.m_hBitmap.ToInt32();
}
}
public virtual void OnCreate(object hook) {
if (hook != null) {
this.m_application = (IApplication)hook;
}
}
public string Caption {
get { return this.m_caption; }
}
public string Tooltip {
get { return this.m_tooltip; }
}
public int HelpContextID {
get { return 0; }
}
public string Name {
get { return this.m_category.Replace(" ", "") + "_" + this.m_caption.Replace(" ", ""); }
}
public virtual bool Checked {
get { return false; }
}
public virtual bool Enabled {
get { return true; }
}
public string HelpFile {
get { return null; }
}
public string Category {
get { return this.m_category; }
}
}
// List of ESRI Guids
[ComVisible(false)]
public static class EsriRegistry {
public const string COMPONENT_MX_COMMAND = "{B56A7C42-83D4-11D2-A2E9-080009B6F22B}";
public const string COMPONENT_MX_TOOLBAR = "{B56A7C4A-83D4-11D2-A2E9-080009B6F22B}";
}
// List of hardcoded application guids
[ComVisible(false)]
public static class ApplicationGuid {
public const string GUID_COMMAND_TEST = "D4E1C9EB-F2E4-4BD1-AF13-3868E555D322";
public const string GUID_TOOLBAR_TEST = "2B8966B6-C623-4F83-A376-53042F667B13";
}
}