AutoCAD .NET

June 17, 2009

A new project wizard for AutoCAD .NET development

Some of you may remember a philosophical question I raised in a post some time ago. The overall consensus of the feedback we received was to prioritise delivering code samples (given a choice between code samples and wizard-like code-generation tools). Which we are doing, both via the ADN site and our developer blogs… but that doesn’t mean some investment in code-generation tools isn’t also appropriate, from time-to-time. :-)

Cyrille Fauvel, who manages the Media & Entertainment arm of DevTech but also occasionally works on technical activities that relate to our other products, has put together a new (currently draft) version of the AutoCAD .NET Wizard. This tool integrates with the Visual Studio 2008 IDE (as well as the 2008 editions of Visual C# Express and Visual Basic Express), adding C#/VB.NET project templates for AutoCAD development. (This version of the tool only works with the 2008 versions – we’re currently evaluating whether (and how) to support the 2005 versions of Visual Studio [Express] or simply to recommend using our previously posted templates.)

When you select one of these “Autodesk” project templates you will get a skeleton project that defines the application initialization protocol plus a few placeholder commands while setting up the ability to define localized commands.

Here are a few images stepping you through the installation of this Wizard (nothing very surprising, which is why I’ve kept the thumbnails so small :-):

AutoCAD 2010 NET Wizards install AutoCAD 2010 NET Wizards install location AutoCAD 2010 NET Wizards install confirmation AutoCAD 2010 NET Wizards install progress AutoCAD 2010 NET Wizards install complete

[Note: the current installer requests an install location, which is unnecessary. The installer only uses this location for temporary files, removing them afterwards. This step will be removed from a future build of the tool.]

Once installed (and Visual Studio 2008 has been re-started), you will get some new options when you start a new project…

AutoCAD 2010 NET Wizards new project menu item

… that include the “AutoCAD 2010 plug-in” project template:

AutoCAD 2010 NET Wizards new project template

We’ve numbered these templates “2010”, as they provide support for API features delivered with AutoCAD 2010, but if you don’t select these features the created project should work with older versions of AutoCAD. At least that’s the theory. :-)

When you first select one of these project types, you will be asked to select the location of your ObjectARX SDK:

AutoCAD 2010 NET Wizards configurator

You should specify this along with the assemblies you would like to reference for this particular project:

AutoCAD 2010 NET Wizards configurated

On completion of this dialog your skeleton project will be created.

[Something else to note: the tool currently creates projects which allow addition of Windows Presentation Foundation content – to do this we had to add a little piece of XML to the project file that specifies a target .NET Framework version of 3.0 or above as well as a GUID to identify the project as WPF-compatible. This tells Visual Studio to list WPF item-types when you add new items to the project, but I expect this to become another configuration option in a future version as some developers will want to target framework versions where WPF is unavailable.]

Let’s take a look at the skeleton code created for a C# project, reformatted to fit the width of the blog…

First the myPlugin.cs file:

using System;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.EditorInput;

 

// This line is not mandatory, but improves loading performance

[assembly: ExtensionApplication(

  typeof(Autodesk.AutoCAD.AutoCAD_2010_plug_in1.MyPlugin)

)]

 

namespace Autodesk.AutoCAD.AutoCAD_2010_plug_in1

{

 

  // This class is instantiated by AutoCAD once and kept alive for

  // the duration of the session. If you don't do any one time

  // initialization then you should remove this class.

  public class MyPlugin : IExtensionApplication

  {

 

    void IExtensionApplication.Initialize()

    {

      // Add one time initialization here

      // One common scenario is to setup a callback function here

      // that unmanaged code can call.

      // To do this:

      // 1. Export a function from unmanaged code that takes a

      //    function pointer and stores the passed in value in a

      //    global variable.

      // 2. Call this exported function in this function passing

      //    delegate.

      // 3. When unmanaged code needs the services of this managed

      //    module you simply call acrxLoadApp() and by the time

      //    acrxLoadApp returns  global function pointer is

      //    initialized to point to the C# delegate.

      // For more info see:

      //http://msdn2.microsoft.com/en-US/library/5zwkzwf4(VS.80).aspx

      //http://msdn2.microsoft.com/en-us/library/44ey4b32(VS.80).aspx

      //http://msdn2.microsoft.com/en-US/library/7esfatk4.aspx

      // as well as some of the existing AutoCAD managed apps.

 

      // Initialize your plug-in application here

    }

 

    void IExtensionApplication.Terminate()

    {

      // Do plug-in application clean up here

    }

  }

}

And now the myCommands.cs file:

using System;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.EditorInput;

 

// This line is not mandatory, but improves loading performances

[assembly: CommandClass(

  typeof(Autodesk.AutoCAD.AutoCAD_2010_plug_in1.MyCommands)

)]

 

namespace Autodesk.AutoCAD.AutoCAD_2010_plug_in1

{

 

  // This class is instantiated by AutoCAD for each document when

  // a command is called by the user the first time in the context

  // of a given document. In other words, non static data in this

  // class is implicitly per-document!

  public class MyCommands

  {

    // The CommandMethod attribute can be applied to any public

    // member function of any public class.

    // The function should take no arguments and return nothing.

    // If the method is an intance member then the enclosing class is

    // intantiated for each document. If the member is a static

    // member then the enclosing class is NOT intantiated.

    //

    // NOTE: CommandMethod has overloads where you can provide helpid

    // and context menu.

 

    // Modal Command with localized name

    [CommandMethod(

      "MyGroup", "MyCommand", "MyCommandLocal", CommandFlags.Modal

    )]

    public void MyCommand() // This method can have any name

    {

      // Put your command code here

    }

 

    // Modal Command with pickfirst selection

    [CommandMethod(

      "MyGroup", "MyPickFirst", "MyPickFirstLocal",

      CommandFlags.Modal | CommandFlags.UsePickSet

    )]

    public void MyPickFirst() // This method can have any name

    {

      PromptSelectionResult result =

        Application.DocumentManager.MdiActiveDocument.Editor.

          SelectImplied();

      if (result.Status == PromptStatus.OK)

      {

        // There are selected entities

        // Put your command using pickfirst set code here

      }

      else

      {

        // There are no selected entities

        // Put your command code here

      }

    }

 

    // Application Session Command with localized name

    [CommandMethod(

      "MyGroup", "MySessionCmd", "MySessionCmdLocal",

      CommandFlags.Modal | CommandFlags.Session

    )]

    public void MySessionCmd() // This method can have any name

    {

      // Put your command code here

    }

 

    // LispFunction is similar to CommandMethod but it creates a

    // lisp callable function. Many return types are supported not

    // just string or integer.

    [LispFunction("MyLispFunction", "MyLispFunctionLocal")]

    public int MyLispFunction(ResultBuffer args)

    // This method can have any name

    {

      // Put your command code here

 

      // Return a value to the AutoCAD Lisp Interpreter

      return 1;

    }

  }

}

The myCommands.resx file needed to define the local command names (for the MySessionCmd command) has also been added to the project automatically to help with command localization.

All well and good… but it may be that these project templates don’t quite fit your internal needs, whether due to the copyright notices in the source and assembly properties [something I do expect to be removed from a future build – it doesn’t make sense for Autodesk copyright notices to be placed in your project skeleton] or because you have a standard approach you’ve adopted for command registration (or whatever). The good news is that it’s pretty easy for you to modify these baseline templates for your own needs:

  1. Locate the template you wish to update, e.g. “C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\ProjectTemplates\CSharp\Autodesk\AutoCAD 2010 plug-in.zip” (this path will vary if you’re running on a 64-bit or localized OS or if you’re using an Express version of Visual Studio, but the technique should still work).
  2. Unzip the files somewhere.
  3. Modify the source code and project setup to meet your needs.
  4. Modify the template file (MyTemplate.vstemplate – an XML file providing Visual Studio with additional information not stored in the project), as needed.
  5. ZIP the files back up, choosing a new name for the .ZIP archive.
  6. Post the file within the ProjectTemplates folder structure.
  7. Run Visual Studio with the /InstallVSTemplates command parameter (the easiest way is to open a “Visual Studio 2008 Command Prompt” and use it to execute “devenv /InstallVSTemplates”).

As mentioned a few times, I do expect us to provide a new version of this tool with some minor wrinkles ironed out, but it’s pretty much ready-to-go (and certainly ready for people to try out and respond with their feedback :-). Please let us know how you get on, whether by posting a comment or by emailing our wizard developers.

June 15, 2009

Highlighting named blocks using AutoCAD 2010’s overrule API from .NET

This is a nice sample provided by Stephen Preston, who manages DevTech’s Americas team. Stephen has put this together in anticipation of his upcoming AU class on the overrule API introduced in AutoCAD 2010. [I know the final class list has not yet been announced, but Stephen is co-owner of the Customization & Programming track at this year’s AU and presumably has the inside skinny on the selected classes. Which means he has a head-start on preparing his material, lucky fellow. :-)]

The sample allows the user to enter a text string that it uses to highlight any block containing that string in its name. This is quite handy for identifying the instances of a particular block in a drawing, but it might also be modified to highlight other objects (you might want to highlight mis-spelt words or standards violations, for instance).

Here’s the C# code, reformatted for this blog:

using System;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.GraphicsInterface;

 

namespace MyCustomFilterOverrule

{

  // This is our custom DrawableOverrule class. We're just

  // overruling WorldDraw and IsApplicable.

  // This class is implemented as a singleton class, and

  // includes subroutines that are called by the CommandMethods

  // in another class

 

  public class MyDrawOverrule : DrawableOverrule

  {

    // Where properties have been defined, use the property rather

    // than the raw variable.

    // I'm using properties where I need some additional logic to

    // run as I get/set the variable.

 

    // The text we'll search for in our block name.

 

    private string mTxt;

 

    // Color Index of block highlight

 

    private short mColor = 3;

 

    // Used to track whether this Overrule has been registered

    // (so we don't try to register it more than once).

 

    private bool mRegistered = false;

 

    // Used to store one and only instance of our singleton class

 

    private static MyDrawOverrule mSingleton;

 

    // Used to reset Overruling value to the value it had before

    // we switched them on. (There may be other overrules in place)

 

    private static bool mOldOverruleValue;

 

    // The color we highlight blocks with

    private short HighlightColor

    {

      get { return mColor; }

      set { if (value >= 0 && value <= 127) mColor = value; }

    }

 

    // The text we'll search for in the block name

 

    private string SearchText

    {

      get { return mTxt; }

      set { mTxt = value; }

    }

 

    // Private constructor because its a singleton

 

    private MyDrawOverrule()

    {

      // Do nothing

    }

 

    // Shared propery to return our singleton instance

    // (and instantiate new instance on first call)

 

    public static MyDrawOverrule GetInstance

    {

      get

      {

        if (mSingleton == null)

        {

          mSingleton = new MyDrawOverrule();

        }

        return mSingleton;

      }

    }

 

    private void InitOverrule()

    {

      if (!mRegistered)

      {

        Overrule.AddOverrule(

          RXObject.GetClass(typeof(BlockReference)), this, false

        );

        SetCustomFilter();

        mOldOverruleValue = Overrule.Overruling;

        mRegistered = true;

      }

      Overrule.Overruling = true;

    }

 

    // Prompts user to select the color index they want to

    // highlight blocks with

 

    public void SetColor()

    {

      Editor ed =

        Application.DocumentManager.MdiActiveDocument.Editor;

 

      PromptIntegerOptions opts =

        new PromptIntegerOptions(

          "\nEnter block finder color index: "

        );

      opts.DefaultValue = HighlightColor;

      opts.LowerLimit = 0;

      opts.UpperLimit = 127;

      opts.UseDefaultValue = true;

      PromptIntegerResult res = ed.GetInteger(opts);

 

      // If requested highlight color is a new color,

      // then we want to change it

 

      if (res.Status == PromptStatus.OK &&

          HighlightColor != res.Value)

      {

        HighlightColor = (short)res.Value;

 

        // Regen is required to update changes on screen

 

        ed.Regen();

      }

    }

 

    public void FindText()

    {

      Editor ed =

        Application.DocumentManager.MdiActiveDocument.Editor;

 

      ed.WriteMessage(

        "\nCurrent block search text is \"{0}\".", SearchText

      );

      PromptStringOptions opts =

        new PromptStringOptions(

          "\nEnter new block search text: "

        );

      PromptResult res = ed.GetString(opts);

 

      // If the user cancelled then we exit the command

 

      if (res.Status != PromptStatus.OK)

        return;

 

      // If the user didn't type any text then we remove

      // the overrule and exit

 

      if (res.StringResult == "")

      {

        SearchText = "";

        ResetBlocks();

      }

      else

      {

        // Set search text for Overrule to that entered by user

 

        SearchText = res.StringResult.ToUpper();

        InitOverrule();

 

        // Turn Overruling on

 

        Overrule.Overruling = true;

 

        // Regen is required to update changes on screen.

 

        ed.Regen();

      }

    }

 

    // Removes our overrules

 

    public void ResetBlocks()

    {

      Editor ed =

        Application.DocumentManager.MdiActiveDocument.Editor;

 

      Overrule.Overruling = mOldOverruleValue;

      if (mRegistered)

      {

        Overrule.RemoveOverrule(

          RXObject.GetClass(typeof(BlockReference)), this

        );

        mRegistered = false;

        ed.Regen();

      }

    }

 

    // Overrule WorldDraw so we can draw our additional

    // graphics

 

    public override bool WorldDraw(Drawable drawable, WorldDraw wd)

    {

      // Better safe than sorry - check it really is a

      // BlockReference before continuing.

 

      BlockReference br = drawable as BlockReference;

      if (br != null)

      {

        // Now we want to draw a green box around the attributes

        // extents

 

        Extents3d ext = (Extents3d)br.Bounds;

        Point3d maxPt = ext.MaxPoint;

        Point3d minPt = ext.MinPoint;

        Point3dCollection pts = new Point3dCollection();

 

        // These are the vertices of the highlight box

 

        pts.Add(new Point3d(minPt.X, minPt.Y, minPt.Z));

        pts.Add(new Point3d(minPt.X, maxPt.Y, minPt.Z));

        pts.Add(new Point3d(maxPt.X, maxPt.Y, minPt.Z));

        pts.Add(new Point3d(maxPt.X, minPt.Y, minPt.Z));

 

        // Store current filltype and set to FillAlways

 

        FillType oldFillType = wd.SubEntityTraits.FillType;

        wd.SubEntityTraits.FillType = FillType.FillAlways;

 

        // Store old graphics color and set to the color we want

 

        short oldColor = wd.SubEntityTraits.Color;

        wd.SubEntityTraits.Color = HighlightColor;

 

        // Draw the filled polygon

 

        wd.Geometry.Polygon(pts);

 

        // Restore old settings

 

        wd.SubEntityTraits.FillType = oldFillType;

        wd.SubEntityTraits.Color = oldColor;

      }

 

      // Let the overruled Drawable draw itself.

 

      return base.WorldDraw(drawable, wd);

    }

 

    // This function is called if we call SetCustomFilter on our

    // custom overrule.

    // We add our own code to return true if the BlockReference

    // passed in is one we want to highlight.

 

    public override bool IsApplicable(RXObject overruledSubject)

    {

      // If it's a BlockReference, we check if the Block Name

      // contains our string

 

      BlockReference br = overruledSubject as BlockReference;

      if (br != null && SearchText != "")

      {

        // Returns whether the filter is applicable to this object

 

        return br.Name.Contains(SearchText);

      }

 

      // Only get to here if object isn't a BlockReference

 

      return false;

    }

  }

 

  // Our command class, which relays commands to MyDrawOverrule.

 

  public class myPlugin

  {

    [CommandMethod("SHOWBLOCKS")]

    public static void FindText()

    {

      MyDrawOverrule.GetInstance.FindText();

    }

 

    [CommandMethod("SHOWCOLOR")]

    public static void SetColor()

    {

      MyDrawOverrule.GetInstance.SetColor();

    }

  }

}

Here’s what happens when we OPEN the “Mechanical – Multileaders.dwg” sample drawing, NETLOAD our application and use SHOWBLOCKS to look for the “M045” text string:

Highlighted blocks

Here’s what we see if we broaden the search to include all blocks with the string “M0” in their name and change the highlight colour to 1 using SHOWCOLOR:

More highlighted blocks

To clear the selection, the user simply has to run SHOWBLOCKS and specify an empty string as the search term.

Stephen will be presenting both C# and VB.NET versions of this sample application during his class at this year’s AU. If you find overrules interesting, then I strongly recommend signing up for the session (I’ll let you know when registrations are open). I’m sure that during the class Stephen will be demonstrating other interesting capabilities made available to AutoCAD .NET developers by this very cool API.

June 10, 2009

Updated DevTV: AutoCAD VBA to .NET Migration Basics

Stephen Preston has updated his “AutoCAD VBA to .NET Migration Basics” DevTV session.

I’ve updated the links in this previous post to point to the updated DevTV, as the original recording is likely to disappear during the coming days.

Here’s an update from Stephen on what’s new in this version:

New features of the migration tool:

  • Automatically adds ‘ObjectDBX’ Type Library to created VB6 project file to ensure Autodesk.AutoCAD.Interop.Common classes are correctly identified (previously these came in as Autodesk.AutoCAD.Interop)
  • Extra functionality to post-process your VB Express project for you:
    - Adds F5 startup application
    - Adds references to acmgd and acdbmgd dlls from the ObjectARX SDK
    - Sets project to use .NET Framework 3.5.

The first part of the recording is the same, but I’ve added a more detailed explanation of the migration tool functionality, and re-recorded the demos to include event handling.

Please check it out and pass on your feedback to Stephen - the comments he received about the original version were instrumental in steering this update.

June 08, 2009

Registering AutoCAD commands with localized names using .NET

While I was preparing this recent post I put together a simple project which registered localized names for commands, to make sure they were picked up and used to create the appropriate demand-loading Registry keys. It was a little tricky to do this from the current documentation, so thankfully I had access to this DevNote on the ADN site which helped a great deal (this is only available to ADN members but don’t worry if you’re not one: this post should provide equivalent information and in certain ways goes beyond the original example).

Anyway, it seemed a relevant topic to cover in its own post, so here we are today looking at this question: how to register localized command-names – possibly for multiple target languages – for your AutoCAD commands.

First of all we need to register some commands, the code for which proves to be pretty straightforward. Here’s the C# code defining our commands, which we will save in a file named Commands.cs:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

 

namespace LocalizedCommands

{

  public class LocCmds

  {

    [CommandMethod(

      "LOCCMDS", "HELLO", "helloCmdId", CommandFlags.Modal

    )]

    public void HelloCommand()

    {

      Application.DocumentManager.MdiActiveDocument.Editor.

        WriteMessage("\nHello!");

    }

 

    [CommandMethod(

      "LOCCMDS", "GOODBYE", "goodbyeCmdId", CommandFlags.Modal

    )]

    public void GoodbyeCommand()

    {

      Application.DocumentManager.MdiActiveDocument.Editor.

        WriteMessage("\nGoodbye!");

    }

  }

}

You’ll notice straight away that we’re using a special version of the CommandMethod attribute which specifies more than the usual items (command name and flags are the most common ones). We start with the command group (“LOCCMDS”), the global names (“HELLO” and “GOODBYE”), the localized resource IDs (“helloCmdId” and “goodbyeCmdId”) and then come the flags (CommandFlags.Modal).

The localized command name is provided as a resource ID to aid localization. Let’s see how this helps… we’ll add some resources to the project for our neutral culture (US English) and for two additional cultures (French and German).

We can add resource files by right-clicking the project and selecting Add –> New Item:

Add a new item to the project

From here we can use the Add New Item dialog to add new resource files with the names “Commands.resx” (which is for the neutral culture, US English), “Commands.fr-FR.resx” and “Commands.de-DE.resx” (these three files will need to be added one-by-one). It’s important that the resource files use the same base name as the .cs file (they do not have to use the class name – LocCmds – and, in fact, you may run into name collisions if you attempt to do so).

Adding a new resource file

Our solution

 

So far, so good. We should now see the resultant files in our solution explorer, and be able to open them by double-clicking them.

The files will be blank initially, and we will want to add two string resources to each, one for each of our commands, using the IDs “helloCmdId” and “goodbyeCmdId”.

 

Question

 

When we come to edit the individual string resources, we may be presented with a warning dialog (it is safe to select Yes):

 

Now we can go ahead and add localized string resources for our two commands in each of the three cultures:

Our resources

It should now be possible to build our application. If we take a look at the output folder, we should see sub-folders for each of our cultures:

Directory structure with resource DLLs

The en-US folder will be empty, as we’ve included that as our base culture, but the other two will contain resource DLLs (also known as satellite assemblies) containing our localized strings for that target culture.

Now to see these in action. In order to fake the loading of our assembly into different language versions of AutoCAD, I put together a simple application to change the UI culture (the one used by the resource manager to choose the resources to load) of the current thread. This clearly needs to be in a separate assembly, as we want to change the culture before we use NETLOAD to load the assembly containing our localized commands.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

using System.Threading;

using System.Globalization;

 

namespace CultureShift

{

  public class Commands

  {

    private void setCulture(string culture)

    {

      Thread.CurrentThread.CurrentUICulture =

        new CultureInfo(culture);

    }

 

    private void setNeutralCulture(string neutCult)

    {

      Thread.CurrentThread.CurrentUICulture =

        CultureInfo.CreateSpecificCulture(neutCult);

    }

 

    [CommandMethod("SETFR")]

    public void SetFrenchCulture()

    {

      setNeutralCulture("fr"); // or setCulture("fr-FR");

      GetCulture();

    }

 

    [CommandMethod("SETDE")]

    public void SetGermanCulture()

    {

      setNeutralCulture("de"); // or setCulture("de-DE");

      GetCulture();

    }

 

    [CommandMethod("SETEN")]

    public void SetEnglishCulture()

    {

      setNeutralCulture("en"); // or setCulture("en-US");

      GetCulture();

    }

 

    [CommandMethod("GETCUL")]

    public void GetCulture()

    {

      Application.DocumentManager.MdiActiveDocument.Editor.

        WriteMessage(

          "\nCurrent UI culture is {0}.",

          Thread.CurrentThread.CurrentUICulture.Name

        );

    }

  }

}

This code implements a number of commands to set the UI culture to US English (SETEN), French (SETFR) and German (SETDE) as well as to check the current UI culture (GETCUL). The code chooses to set the culture neutrally – which sets the language but not the location – but the end result is the same for the cultures we’ve chosen. One thing to bear in mind: this code only actually works if running from the debugger. It doesn’t crash, otherwise, but the current UI culture always ends up as “en-US”. I assume this is a threading issue, but as this is really only for testing purposes it’s a tolerable requirement to run everything from the debugger.

Here’s what happens when (launching AutoCAD from the Visual Studio 2009 debugger) we load our test code, set the current UI culture to French and then load the main application, checking which commands work and which do not:

Command: NETLOAD

Command: GETCUL

Current UI culture is en-US.

Command: SETFR

Current UI culture is fr-FR.

Command: GETCUL

Current UI culture is fr-FR.

Command: NETLOAD

Command: HELLO

Hello!

Command: GOODBYE

Goodbye!

Command: BONJOUR

Hello!

Command: AUREVOIR

Goodbye!

Command: GUTENTAG Unknown command "GUTENTAG".  Press F1 for help.

Command: BYE Unknown command "BYE".  Press F1 for help.

We can see that the global and the French-localized commands have indeed worked, while the German and US ones have not.

Let’s do the same for German:

Command: NETLOAD

Command: GETCUL

Current UI culture is en-US.

Command: SETDE

Current UI culture is de-DE.

Command: GETCUL

Current UI culture is de-DE.

Command: NETLOAD

Command: HELLO

Hello!

Command: GOODBYE

Goodbye!

Command: BONJOUR Unknown command "BONJOUR".  Press F1 for help.

Command: GUTENTAG

Hello!

Command: AUFWIEDERSEHEN

Goodbye!

Command: BYE Unknown command "BYE".  Press F1 for help.

And sure enough, while the German and global commands work, the others do not.

For completeness, if we just load our application – without either launching from the debugger or loading our test application to change the current UI culture – and run the commands, here’s what we see:

Command: NETLOAD

Command: HELLO

Hello!

Command: GOODBYE

Goodbye!

Command: HI

Hello!

Command: BYE

Goodbye!

Command: BONJOUR Unknown command "BONJOUR".  Press F1 for help.

Command: AUREVOIR Unknown command "AUREVOIR".  Press F1 for help.

Command: GUTENTAG Unknown command "GUTENTAG".  Press F1 for help.

Command: AUFWIEDERSEHEN Unknown command "AUFWIEDERSEHEN".  Press F1 for help.

My assumption is that the current UI culture will be set appropriately in the different language versions of AutoCAD, and so the localized resources will be chosen correctly. If someone trying the technique for real were able to confirm, I'd certainly appreciate it. :-)

June 04, 2009

Creating Fibonacci spirals in AutoCAD using F#

I recently stumbled across this post which inspired me to do something similar in AutoCAD (the fact that both posts cover Fibonacci spirals and use F# is about where the similarity ends - they do things quite differently).

Fibonacci spirals are an approximation of the golden spiral, which for old timers out there will be reminiscent of the AutoCAD R12 (it was R12, wasn’t it?) design collateral - the same as this one from AME 2.1 - which I still find cool after all these years. :-)

The first thing was to create a function that returns a portion of the Fibonacci sequence:

let fibs n =

  Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) (0I,1I)

  |> Seq.take n

  |> Seq.to_list

A few comments about this implementation:

  • I searched online for tail-recursive Fibonacci implementations (not that we’re likely to create a stack overflow with the number of recursions we’re going to do, but I like to do things right when I can :-)
    • Tail-recursive solutions are easy if returning a specific number, but as we need to return a portion of the Fibonacci sequence things get a little more complicated
    • Here’s a quick reminder of how we can check that tail call optimization has happened, when we do choose to use tail recursion
  • I ended up going for a lazily-evalulated solution, copied and modified from the Foundations of F# book by Robert Pickering
    • Unfold (OK – I admit this Wikipedia entry is beyond obscure for most of us mere mortals – this post may be of more help) applies a function to a seed value to create what may be an infinite sequence of numbers (which is precisely what the Fibonacci sequence is, of course)
    • We use the Seq data-type (essentially an IEnumerable in .NET) which is lazy
      • This means that it only actually evaluates the various items in the list as we ask for them
    • We then “take” the first n items from the list (n will be specified by the user), so only that number of items get evaluated
    • We convert the results to a list to return to the caller
  • I like the elegance of this solution and it’s certainly efficient enough for our purposes

If you load this code into F# interactive and execute it against the first 20 numbers in the sequence, you get:

> fibs 20;;

val it : bigint list

= [0I; 1I; 1I; 2I; 3I; 5I; 8I; 13I; 21I; 34I; 55I; 89I; 144I; 233I; 377I; 610I;

  987I; 1597I; 2584I; 4181I]

Otherwise the below implementation should be reasonably straightforward. We define a local addSegment function which we then call on each member of the subset of the Fibonacci sequence (in reverse, as we’re drawing the curves from large to small). We use the iteri function to do this, as it provides us with a useful index into the list which then allows us to decide which of the four directions we’re facing (the orientation of the arc rotates 90 degrees each time).

Here’s the complete F# code:

#light

 

// Declare our namespace and module name

 

module Fibonacci.Spiral

 

// Import managed assemblies

 

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open System

 

// A lazy Fibonacci sequence generator

 

let fibs n =

  Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) (0I,1I)

  |> Seq.take n

  |> Seq.to_list

 

[<CommandMethod("fib")>]

let fibonacciSpiral() =

 

  // Let's get the usual helpful AutoCAD objects

 

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

 

  // Ask the user how deep to go

 

  let pio =

    new PromptIntegerOptions("\nEnter number of levels: ")

  pio.AllowNone <- true

  pio.AllowZero <- false

  pio.AllowNegative <- false

  pio.DefaultValue <- 10

  pio.LowerLimit <- 1

  pio.UpperLimit <- 50

  pio.UseDefaultValue <- true

 

  let pir = ed.GetInteger(pio)

 

  if pir.Status = PromptStatus.OK then

 

    // We'll actually add one to the value provided

    // as this gives more logical results

 

    let levels = pir.Value + 1

 

    // "use" has the same effect as "using" in C#

 

    use tr =

      db.TransactionManager.StartTransaction()

 

    // Get appropriately-typed BlockTable and BTRs

 

    let bt =

      tr.GetObject

        (db.BlockTableId,OpenMode.ForRead)

      :?> BlockTable

 

    let ms =

      tr.GetObject

        (bt.[BlockTableRecord.ModelSpace],

        OpenMode.ForWrite)

      :?> BlockTableRecord

 

    // Create our polyline, set its defaults,

    // add it to the modelspace and the transaction

 

    let pl = new Polyline()

    pl.SetDatabaseDefaults()

    ms.AppendEntity(pl) |> ignore

    tr.AddNewlyCreatedDBObject(pl, true)

    pl.AddVertexAt

      (pl.NumberOfVertices, Point2d.Origin, 0.0, 0.0, 0.0)

 

    // We need a mutable start point variable for

    // each of our arcs to connect

 

    let start = ref Point3d.Origin

 

    // Add an arc segment to our polyline

 

    let addSegment i size =

 

      // i is the index in the list provided by iteri

 

      // Decide the directions of the "axes" and the arc's start

      // angle based on one of four possibilities

 

      let (xdir, ydir, startAngle) =

        match (i % 4) with

        | 0 -> (Vector3d.XAxis, Vector3d.YAxis, 0.0)

        | 1 -> (-Vector3d.YAxis, Vector3d.XAxis, Math.PI * 1.5)

        | 2 -> (-Vector3d.XAxis, -Vector3d.YAxis, Math.PI)

        | 3 -> (Vector3d.YAxis, -Vector3d.XAxis, Math.PI / 2.0)

        | _ -> failwith "Invalid modulus remainder!"

 

      // The end angle is 90 degrees from the start

 

      let endAngle = startAngle + Math.PI / 2.0

 

      // The center of the arc is bottom right-hand corner

      // of the box (direction goes along the bottom from

      // left to right, so we go "size" units along the

      // direction from the start point)

 

      let center = !start + xdir * float size

 

      // Bulge is defined as the tan of one quarter of the

      // included angle (and negative, as we're going

      // clockwise)

 

      let bulge = Math.Tan((endAngle - startAngle) / -4.0)

 

      // We need to convert our 3D start point to a 2D point

      // on the plane of the polyline

 

      let pos = (!start).Convert2d(pl.GetPlane())

 

      // Now we just add the vertex at the end and mutate our

      // start variable to contain the end of the arc

 

      pl.AddVertexAt(pl.NumberOfVertices, pos, bulge, 0.0, 0.0)

      start.contents <- center + ydir * float size

 

    // Here's where we plug it all together...

 

    // Get the first n fibonacci numbers, reverse the list and

    // call our function on each one (passing its index along,

    // too)

 

    fibs levels |> List.rev |> List.iteri addSegment

    tr.Commit()

Here’s what happens when we run the FIB command and select levels 1 to 8 (we need to call FIB eight times to do this), creating eight different Fibonacci spirals:

Levels 1 to 8 of our Fibonacci spiral

And here’s the result for level 50, as a comparison (although unless you zoom right in it might as well be level 20):

Level 50 Fibonacci spiral

June 02, 2009

Winner of the F# programming contest

Back at the beginning of the year I launched a programming contest for using F# with Autodesk products. A few months ago I introduced one of the winning entries showing how to use F# to implement Overrules in AutoCAD 2010. Now I’ve finally got around to unveiling the second winning entry.

And the winner is… drumroll…

Nada Amin, who entered her Master of Engineering project which uses F# with AutoCAD. Here is a description of the project from the MIT website:

Micado: an AutoCAD plug-in for programmable microfluidic chips

Programmable microfluidics, using multi-layer soft lithography, are lab-on-chip systems that can automate biological computations or experiments by integrating a diverse set of biological sensors and by manipulating fluids at the picoliter scale.

Micado is a Computer-Aided Design (CAD) tool for designing and controlling programmable microfluidics, featuring:

  • standard and customizable design rules
  • automatic routing between control valves and punches
  • automatic generation of control instructions and GUI

The code is hosted here (under which you will find the majority of the code having been written in F#).

Nada is now based over in Zürich, so I hope someday to meet with her to discuss the project in person, but in the meantime I’ll be shipping across a copy of Expert F#.

Congratulations, Nada! :-)

May 29, 2009

Free API webcasts on using Windows Presentation Foundation (WPF) in your AutoCAD applications

Fenton Webb, from DevTech Americas, is presenting two sessions on using WPF in your AutoCAD .NET applications. These will be of particular interest to developers wanting to take their ribbon interfaces to the next level in AutoCAD 2010, but will be of relevance to anyone wanting to understand more about WPF and how it can be used within AutoCAD.

The first session is coming up quickly, on June 2nd (next Tuesday), but if you’re reading this soon after I’ve posted you should still have time to register. The second session is scheduled for July 14th.

Both sessions will be recorded and available for download from our API training schedule which can also be accessed via this easy-to-remember URL: http://www.autodesk.com/apitraining.

May 28, 2009

Creating demand-loading entries automatically for your AutoCAD application using .NET

Here’s a question I received recently by email:

How do you set up a .NET plugin for AutoCAD to install & demand load in the same way as ObjectARX plugins? The documentation is not very clear at making the distinctions visible.

In ARX terms, we currently write a set of registry entries as part of our installer, along with refreshing these via an AcadAppInfo registration during ARX load. The ARX itself can be located anywhere as long as the registry entries point to it. I’m not sure of the correct procedure for .NET plugins to duplicate this.

Augusto Gonçalves, from our DevTech Americas team, provided a solution for this which showed how to create demand-loading Registry keys programmatically based on the current assembly’s name and location. It occurred to me that extending the code to make further use of reflection to query the commands defined by an assembly would make this really interesting, and could essentially create a very flexible approach for creation of a demand-loading entries as an application initializes or during execution of a custom command.

Here’s the C# code:

using System.Collections.Generic;

using System.Reflection;

using System.Resources;

using System;

using Microsoft.Win32;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

 

namespace DemandLoading

{

  public class RegistryUpdate

  {

    public static void RegisterForDemandLoading()

    {

      // Get the assembly, its name and location

 

      Assembly assem = Assembly.GetExecutingAssembly();

      string name = assem.GetName().Name;

      string path = assem.Location;

 

      // We'll collect information on the commands

      // (we could have used a map or a more complex

      // container for the global and localized names

      // - the assumption is we will have an equal

      // number of each with possibly fewer groups)

 

      List<string> globCmds = new List<string>();

      List<string> locCmds = new List<string>();

      List<string> groups = new List<string>();

 

      // Iterate through the modules in the assembly

 

      Module[] mods = assem.GetModules(true);

      foreach(Module mod in mods)

      {

        // Within each module, iterate through the types

 

        Type[] types = mod.GetTypes();

        foreach (Type type in types)

        {

          // We may need to get a type's resources

 

          ResourceManager rm =

            new ResourceManager(type.FullName, assem);

          rm.IgnoreCase = true;

 

          // Get each method on a type

 

          MethodInfo[] meths = type.GetMethods();

          foreach (MethodInfo meth in meths)

          {

            // Get the methods custom command attribute(s)

 

            object[] attbs =

              meth.GetCustomAttributes(

                typeof(CommandMethodAttribute),

                true

              );

            foreach (object attb in attbs)

            {

              CommandMethodAttribute cma =

                attb as CommandMethodAttribute;

              if (cma != null)

              {

                // And we can finally harvest the information

                // about each command

 

                string globName = cma.GlobalName;

                string locName = globName;

                string lid = cma.LocalizedNameId;

 

                // If we have a localized command ID,

                // let's look it up in our resources

 

                if (lid != null)

                {

                  // Let's put a try-catch block around this

                  // Failure just means we use the global

                  // name twice (the default)

 

                  try

                  {

                    locName = rm.GetString(lid);

                  }

                  catch

                  {}

                }

 

                // Add the information to our data structures

 

                globCmds.Add(globName);

                locCmds.Add(locName);

 

                if (cma.GroupName != null &&

                    !groups.Contains(cma.GroupName))

                  groups.Add(cma.GroupName);

              }

            }

          }

        }

      }

 

      // Let's register the application to load on demand (12)

      // if it contains commands, otherwise we will have it

      // load on AutoCAD startup (2)

 

      int flags = (globCmds.Count > 0 ? 12 : 2);

 

      // By default let's create the commands in HKCU

      // (pass false if we want to create in HKLM)

 

      CreateDemandLoadingEntries(

        name, path, globCmds, locCmds, groups, flags, true

      );

    }

 

    public static void UnregisterForDemandLoading()

    {

      RemoveDemandLoadingEntries(true);

    }

 

    // Helper functions

 

    private static void CreateDemandLoadingEntries(

      string name,

      string path,

      List<string> globCmds,

      List<string> locCmds,

      List<string> groups,

      int flags,

      bool currentUser

    )

    {

      // Choose a Registry hive based on the function input

 

      RegistryKey hive =

        (currentUser ? Registry.CurrentUser : Registry.LocalMachine);

 

      // Open the main AutoCAD (or vertical) and "Applications" keys

 

      RegistryKey ack =

        hive.OpenSubKey(

          HostApplicationServices.Current.RegistryProductRootKey

        );

      RegistryKey appk =

        ack.OpenSubKey("Applications", true);

 

      // Already registered? Just return

 

      string[] subKeys = appk.GetSubKeyNames();

      foreach (string subKey in subKeys)

      {

        if (subKey.Equals(name))

        {

          appk.Close();

          return;

        }

      }

 

      // Create the our application's root key and its values

 

      RegistryKey rk =

        appk.CreateSubKey(name);

      rk.SetValue("DESCRIPTION", name, RegistryValueKind.String);

      rk.SetValue("LOADCTRLS", flags, RegistryValueKind.DWord);

      rk.SetValue("LOADER", path, RegistryValueKind.String);

      rk.SetValue("MANAGED", 1, RegistryValueKind.DWord);

 

      // Create a subkey if there are any commands...

 

      if ((globCmds.Count == locCmds.Count) &&

          globCmds.Count > 0)

      {

        RegistryKey ck =

          rk.CreateSubKey("Commands");

 

        for (int i=0; i < globCmds.Count; i++)

          ck.SetValue(

            globCmds[i],

            locCmds[i],

            RegistryValueKind.String

          );

      }

 

      // And the command groups, if there are any

 

      if (groups.Count > 0)

      {

        RegistryKey gk =

          rk.CreateSubKey("Groups");

 

        foreach (string grpName in groups)

          gk.SetValue(grpName, grpName, RegistryValueKind.String);

      }

 

      appk.Close();

    }

 

    private static void RemoveDemandLoadingEntries(bool currentUser)

    {

      // Choose a Registry hive based on the function input

 

      RegistryKey hive =

        (currentUser ? Registry.CurrentUser : Registry.LocalMachine);

 

      // Open the main AutoCAD (or vertical) and "Applications" keys

 

      RegistryKey ack =

        hive.OpenSubKey(

          HostApplicationServices.Current.RegistryProductRootKey

        );

      RegistryKey appk =

        ack.OpenSubKey("Applications", true);

 

      // Delete the key with the same name as this assembly

 

      appk.DeleteSubKeyTree(

        Assembly.GetExecutingAssembly().GetName().Name

      );

      appk.Close();

    }

  }

}

If you drop this code into an existing project, you should be able simply to add a call to DemandLoading.RegistryUpdate.RegisterForDemandLoading() during your IExtensionApplication’s Initialize() method or during a custom command.

Here’s an example of the Registry keys created by this code (exported from Regedit) when called from the Initialize() method of the application in this previous post:

Windows Registry Editor Version 5.00

 

[HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R18.0\ACAD-8001:409\Applications\OffsetInXref]

"DESCRIPTION"="OffsetInXref"

"LOADCTRLS"=dword:0000000c

"LOADER"="C:\\Program Files\\Autodesk\\AutoCAD 2010\\OffsetInXref.dll"

"MANAGED"=dword:00000001

 

[HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R18.0\ACAD-8001:409\Applications\OffsetInXref\Commands]

"XOFFSETLAYER"="XOFFSETLAYER"

"XOFFSETCPLAYS"="XOFFSETCPLAYS"

 

You can see that our code found the commands defined by the application and created Registry entries which tell AutoCAD to load the module when one of them is chosen by the user. You’ll notice the LOADCTRLS value is c (hexadecimal for 12), which means the application will be loaded “on demand” as a specified command is invoked, but we could also adjust the code to force this to 2, which would mean the module would be loaded on AutoCAD startup (the default when no commands are found). This would actually be a good idea, in this case, as the command hooks into the OFFSET command, and we can’t demand-load a module on invocation of a built-in command.

You’ll also notice that the keys were created under R18.0\ACAD-8001:409 (the English version of AutoCAD 2010), but if the module was loaded in a different language version of an AutoCAD-based vertical product (French AutoCAD Architecture 2009, for instance) then the root key would be the one for that product. All you have to do is load the module once in the AutoCAD-based product of your choice, and it will be registered for automatic loading from then onwards.

This is a useful technique for people who want to deploy .NET modules without installers, or for people who wish applications to re-create their demand-loading keys on load (something this code currently does not do, by the way: if the application’s key is found we do not recreate the contents for the sake of efficiency… you may want to change the code to force creation of the keys should you be adding new commands regularly to your application, for instance).

May 25, 2009

Interfacing an external COM application with a .NET module in-process to AutoCAD (redux)

Thanks to all of your interest in this recent post, which looked at a way to interface an out-of-process .NET application with an assembly running in-process to AutoCAD. After some obvious functionality gaps were raised, Renze de Waal, one of our ADN members, pointed out a DevNote on the ADN website covering – and more completely addressing – this topic. Shame on me for not checking there before writing the post. Anyway, onwards and upwards…

The information in the DevNote highlights some of the problems I and other people had hit with my previous code, mostly related to the fact it wasn’t executed on the main AutoCAD thread (which meant we were effectively limited in the interactions we had with the AutoCAD application).

To fix this we can derive our application from System.EnterpriseServices.ServicedComponent (also adding an additional project reference to the System.EnterpriseServices .NET assembly). Here is the updated C# code for the LoadableComponent:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using System.Runtime.InteropServices;

using System.EnterpriseServices;

 

namespace LoadableComponent

{

  [Guid("5B5B731C-B37A-4aa2-8E50-42192BD51B17")]

  public interface INumberAddition

  {

    [DispId(1)]

    string AddNumbers(int arg1, double arg2);

  }

 

  [ProgId("LoadableComponent.Commands"),

   Guid("44D8782B-3F60-4cae-B14D-FA060E8A4D01"),

   ClassInterface(ClassInterfaceType.None)]

  public class Commands : ServicedComponent, INumberAddition

  {

    // A simple test command, just to see that commands

    // are loaded properly from the assembly

 

    [CommandMethod("MYCOMMAND")]

    static public void MyCommand()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      ed.WriteMessage("\nTest command executed.");

    }

 

    // A function to add two numbers and create a

    // circle of that radius. It returns a string

    // withthe result of the addition, just to use

    // a different return type

 

    public string AddNumbers(int arg1, double arg2)

    {     

      // During tests it proved unreliable to rely

      // on DocumentManager.MdiActiveDocument

      // (which was null) so we will go from the

      // HostApplicationServices' WorkingDatabase

 

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

 

      ed.WriteMessage(

        "\nAdd numbers called with {0} and {1}.",

        arg1, arg2

      );

 

      // Perform our addition

 

      double res = arg1 + arg2;

 

      // Lock the document before we access it

 

      DocumentLock loc = doc.LockDocument();

      using (loc)

      {

        Transaction tr =

          db.TransactionManager.StartTransaction();

        using (tr)

        {

          // Create our circle

 

          Circle cir =

            new Circle(

              new Point3d(0, 0, 0),

              new Vector3d(0, 0, 1),

              res

            );

 

          cir.SetDatabaseDefaults(db);

 

          // Add it to the current space

 

          BlockTableRecord btr =

            (BlockTableRecord)tr.GetObject(

              db.CurrentSpaceId,

              OpenMode.ForWrite

            );

          btr.AppendEntity(cir);

          tr.AddNewlyCreatedDBObject(cir, true);

 

          // Commit the transaction

 

          tr.Commit();

        }

      }

 

      // Return our string result

 

      return res.ToString();

    }

  }

}

Some points to note...

  • We now use an interface to expose functionality from our component, which allows us more flexibility in the way we return data to the calling application.
  • We're labeling our interface and component with specific GUIDs - generated by guidgen.exe - although we could probably skip this step.
  • We're now able to use the MdiActiveDocument property safely, as well as being able to write messages via the editor.

When we build the component we can - as before - register it via the regasm.exe tool. Here's the .reg output if you specify the /regfile option:

REGEDIT4

 

[HKEY_CLASSES_ROOT\LoadableComponent.Commands]

@="LoadableComponent.Commands"

 

[HKEY_CLASSES_ROOT\LoadableComponent.Commands\CLSID]

@="{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}"

 

[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}]

@="LoadableComponent.Commands"

 

[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\InprocServer32]

@="mscoree.dll"

"ThreadingModel"="Both"

"Class"="LoadableComponent.Commands"

"Assembly"="LoadableComponent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"

"RuntimeVersion"="v2.0.50727"

 

[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\InprocServer32\1.0.0.0]

"Class"="LoadableComponent.Commands"

"Assembly"="LoadableComponent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"

"RuntimeVersion"="v2.0.50727"

 

[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\ProgId]

@="LoadableComponent.Commands"

 

[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\Implemented Categories\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]

One thing to mention - I found that the calling application was not able to to cast the returned System.__COMObject to LoadableComponent.INumberAddition unless I updated the project settings to "Register from COM Interop" (near the bottom of the Build tab).

Now for our calling application… here’s the updated C# code:

using Autodesk.AutoCAD.Interop;

using System.Windows.Forms;

using System.Runtime.InteropServices;

using System.Reflection;

using System;

using LoadableComponent;

 

namespace DrivingAutoCAD

{

  public partial class Form1 : Form

  {

    public Form1()

    {

      InitializeComponent();

    }

 

    private void button1_Click(object sender, EventArgs e)

    {

      const string progID = "AutoCAD.Application.18";

 

      AcadApplication acApp = null;

      try

      {

        acApp =

          (AcadApplication)Marshal.GetActiveObject(progID);

      }

      catch

      {

        try

        {

          Type acType =

            Type.GetTypeFromProgID(progID);

          acApp =

            (AcadApplication)Activator.CreateInstance(

              acType,

              true

            );

        }

        catch

        {

          MessageBox.Show(

            "Cannot create object of type \"" +

            progID + "\""

          );

        }

      }

      if (acApp != null)

      {

        try

        {

          // By the time this is reached AutoCAD is fully

          // functional and can be interacted with through code

 

          acApp.Visible = true;

 

          INumberAddition app =

            (INumberAddition)acApp.GetInterfaceObject(

              "LoadableComponent.Commands"

            );

 

          // Now let's call our method

 

          string res = app.AddNumbers(5, 6.3);

 

          acApp.ZoomAll();

 

          MessageBox.Show(

            this,

            "AddNumbers returned: " + res

          );

        }

        catch (Exception ex)

        {

          MessageBox.Show(

            this,

            "Problem executing component: " +

            ex.Message

          );

        }

      }

    }

  }

}

You should be able to see straightaway that it’s simpler – we cast the results of the GetInterfaceObject call to our interface and call the AddNumbers method on it.

And when we execute the code, we can see we’re now able to write to the command-line, as well as getting better results from our ZoomAll():

Result of driving AutoCAD via in- and out-of-proc code (redux)

May 20, 2009

Interfacing an external COM application with a .NET module in-process to AutoCAD

This question came in recently by email from Michael Fichter of Superstructures Engineers and Architects:

Could you suggest an approach that would enable me to drive a .NET function (via COM) that could return a value from .NET back to COM? I have used SendCommand in certain instances where return values were not needed.

Michael’s referring to a technique used in this previous post, which shows how to launch AutoCAD from a .NET executable via COM and then launch a command which can then safely interface with AutoCAD in-process via its managed API.

And yes, this technique is fine if you don’t want to return results, but has limitations if you do. You could populate AutoCAD user variables or create a file for the calling application to read but such approaches are cumbersome.

So… in spite of my initial doubtful reaction I decided to give it a try. Here are the steps I used to get this working…

First we create a Class Library for our in-process component with references to the usual acmgd.dll and acdbmgd.dll assemblies, adding the following C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using System.Runtime.InteropServices;

 

namespace LoadableComponent

{

  [ProgId("LoadableComponent.Commands")]

  public class Commands

  {

    // A simple test command, just to see that commands

    // are loaded properly from the assembly

 

    [CommandMethod("MYCOMMAND")]

    public void MyCommand()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      ed.WriteMessage("\nTest command executed.");

    }

 

    // A function to add two numbers and create a

    // circle of that radius. It returns a string

    // withthe result of the addition, just to use

    // a different return type

 

    public string AddNumbers(int arg1, double arg2)

    {

      // During tests it proved unreliable to rely

      // on DocumentManager.MdiActiveDocument

      // (which was null) so we will go from the

      // HostApplicationServices' WorkingDatabase

 

      Database db =

        HostApplicationServices.WorkingDatabase;

      Document doc =

        Application.DocumentManager.GetDocument(db);

 

      // Perform our addition

 

      double res = arg1 + arg2;

 

      // Lock the document before we access it

 

      DocumentLock loc = doc.LockDocument();

      using (loc)

      {

        Transaction tr =

          db.TransactionManager.StartTransaction();

        using (tr)

        {

          // Create our circle

 

          Circle cir =

            new Circle(

              new Point3d(0, 0, 0),

              new Vector3d(0, 0, 1),

              res

            );

 

          cir.SetDatabaseDefaults(db);

 

          // Add it to the current space

 

          BlockTableRecord btr =

            (BlockTableRecord)tr.GetObject(

              db.CurrentSpaceId,

              OpenMode.ForWrite

            );

          btr.AppendEntity(cir);

          tr.AddNewlyCreatedDBObject(cir, true);

 

          // Commit the transaction

 

          tr.Commit();

        }

      }

 

      // Return our string result

 

      return res.ToString();

    }

  }

}

You’ll see we mark our Commands class as having the ProgId of “LoadableComponent.Commands” (this doesn’t have to follow the namespace.class-name convention, if you’d rather use something else). Be sure to edit the AssemblyInfo.cs file to make sure the ComVisible assembly attribute is set to true (the default is false), otherwise no classes will be exposed via COM.

The code is mostly pretty simple… it includes a command just to make sure commands are registered when the assembly loads. The AddNumbers() function uses a slightly different technique to get the working database and its document, mainly because I found MdiActiveDocument to be null when I needed it. I suspect this is simply a timing issue, and that if AutoCAD had the time to fully initialize we wouldn’t have to code this defensively. There may well be a clean way to wait for this to happen (comments, anyone?).

Once we’ve built the assembly it needs to be registered via COM. The way I tend to do this is via a “Visual Studio Command Prompt” (which has the path set nicely to call the VS development tools). I browse to the location of my assembly and then run “regasm LoadableComponent.dll” (you can specify the optional /reg parameter if you’d rather create a .reg file rather than modifying the Registry directly).

Now we can create an executable project to drive this component with COM references to the AutoCAD Type Library (I’m using the one for AutoCAD 2010) and the AutoCAD/ObjectDBX Common Type Library (AutoCAD 2010’s is version 18.0), as well as a reference to our .NET assembly (which I have called LoadableComponent.dll).

Inside the default form created with the executable project we can add a button behind which we copy the code in the post referred to earlier, adding some logic to load our component and dynamically execute its AddNumbers() function:

using Autodesk.AutoCAD.Interop;

using System.Windows.Forms;

using System.Runtime.InteropServices;

using System.Reflection;

using System;

using LoadableComponent;

 

namespace DrivingAutoCAD

{

  public partial class Form1 : Form

  {

    public Form1()

    {

      InitializeComponent();

    }

 

    private void button1_Click(object sender, EventArgs e)

    {

      const string progID = "AutoCAD.Application.18";

 

      AcadApplication acApp = null;

      try

      {

        acApp =

          (AcadApplication)Marshal.GetActiveObject(progID);

      }

      catch

      {

        try

        {

          Type acType =

            Type.GetTypeFromProgID(progID);

          acApp =

            (AcadApplication)Activator.CreateInstance(

              acType,

              true

            );

        }

        catch

        {

          MessageBox.Show(

            "Cannot create object of type \"" +

            progID + "\""

          );

        }

      }

      if (acApp != null)

      {

        try

        {

          // By the time this is reached AutoCAD is fully

          // functional and can be interacted with through code

 

          acApp.Visible = true;

 

          object app =

            acApp.GetInterfaceObject("LoadableComponent.Commands");

 

          if (app != null)

          {

            // Let's generate the arguments to pass in:

            // an integer and a double

 

            object[] args = { 5, 6.3 };

 

            // Now let's call our method dynamically

 

            object res =

              app.GetType().InvokeMember(

                "AddNumbers",

                BindingFlags.InvokeMethod,

                null,

                app,

                args

              );

            acApp.ZoomAll();

            MessageBox.Show(

              this,

              "AddNumbers returned: " + res.ToString()

            );

          }

        }

        catch (Exception ex)

        {

          MessageBox.Show(

            this,

            "Problem executing component: " +

            ex.Message

          );

        }

      }

    }

  }

}

I decided to try Application.GetInterfaceObject() – the classic way to load an old VB6 ActiveX DLL into AutoCAD from VBA or Visual LISP – to see whether it worked for .NET assemblies that have a ProgId assigned. It not only worked, but the commands contained within the module were registered properly. A nice surprise! :-)

I started by defining an interface in the Class Library to be used in the Executable, but ended up going with a more dynamic approach, using InvokeMember() on the class of the object returned by GetInterfaceObject(). This avoids having to define and cast to the interface but adds a little uncertainty to the operation (as I’ve mentioned a number of times in recent weeks when we start getting dynamic we lose most of the compiler crutches we’ve all become used to :-). I also hit a problem if the command function was declared as static, but presumably that can be resolved with the right arguments to InvokeMember().

When we run this code and select the button, we see a circle get created with the radius of 11.3, the result of adding the integer (5) and double (6.3) we passed to the AddNumbers() function:

Result of driving AutoCAD via in- and out-of-proc code

The combination of COM for out-of-process control with .NET for in-process power and performance will hopefully be a useful technique for many of you needing to automate AutoCAD from an external executable. Be sure to post comments if any of you have things to share on this topic. Thanks for the question, Michael! :-)

Update:

A much-improved implementation of this application can be found in this post.

RSS Feed

Search