« November 2007 | Main | January 2008 »

Launching AutoCAD with a specific profile using .NET

This is a follow-up to this previous post, where we used COM to launch the AutoCAD process. Tony Tanzillo rightly pointed out the code in that post could be simplified slightly, so check out his comment for the specifics.

Today we're going to look at launching AutoCAD more manually, allowing us to control the working folder and specify command-line parameters. This is to address the specific question of choosing an alternative startup profile for the application, but AutoCAD's command-line parameters allow you to do a great deal more than that.

We're going to use the System.Diagnostics namespace to specify our options and launch our process, and then use COM to connect to it (so we can use the COM Automation API, as before, to run commands, etc.).

Here are the namespace declarations:

using System;

using System.Diagnostics;

using System.Runtime.InteropServices;

using Autodesk.AutoCAD.Interop;

And here's the C# code to implement your function or button message-handler:

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


  // The @ means it's a literal string - no need

  // for double backslashes


  const string exePath =

    @"c:\Program Files\Autodesk\AutoCAD 2008\acad.exe";


  AcadApplication acApp = null;


  // Let's first check we don't have AutoCAD already running


  try

  {

    acApp =

      (AcadApplication)Marshal.GetActiveObject(progID);

  }

  catch {}


  if (acApp != null)

  {

    MessageBox.Show(

      "An instance of AutoCAD is already running."

    );

  }

  else

  {

    try

    {

      // Use classes from the System.Diagnostics namespace

      // to launch our AutoCAD process with command-line

      // options


      ProcessStartInfo psi =

        new ProcessStartInfo(exePath, "/p myprofile");

      psi.WorkingDirectory = @"c:\temp";

      Process pr = Process.Start(psi);


      // Wait for AutoCAD to be ready for input

      // This doesn't wait until AutoCAD is ready

      // to receive COM requests, it seems


      pr.WaitForInputIdle();


      // Connect to our process using COM


      // We're going to loop infinitely until we get the

      // AutoCAD object.         

      // A little risky, unless we implement a timeout

      // mechanism or let the user cancel


      while (acApp == null)

      {

        try

        {

          acApp =

            (AcadApplication)Marshal.GetActiveObject(progID);

        }

        catch

        {

          // Let's let the application check its message

          // loop, in case the user has exited or cancelled


          Application.DoEvents();

        }

      }

    }

    catch (Exception ex)

    {

      MessageBox.Show(

        "Cannot create or attach to AutoCAD object: "

        + ex.Message

      );

    }

  }


  if (acApp != null)

  {

    acApp.Visible = true;

    acApp.ActiveDocument.SendCommand("_MYCOMMAND ");

  }

Well, that's it for 2007. A Happy Holidays to you all, and a Merry Christmas & Happy New Year to those of you who celebrate these events.

See you in 2008!

December 21, 2007 in AutoCAD, AutoCAD .NET | Permalink | Comments (6) | TrackBack

Microbenchmarking C# code

Jeremy Tammik, from our DevTech EMEA team, pointed me to this useful and interesting site:

http://www.yoda.arachsys.com/csharp/benchmark.html

It introduces a very easy way to benchmark functions in your application by simply tagging them with a [Benchmark] attribute (you also need to have included the C# file posted on the above site in your project, of course).

Jeremy also highlighted a very pertinent paragraph in the above site:

Use local variables where possible.

The CLR can do a more optimisations on code which doesn't (for the most part) "escape" from just local variables. For instance, it doesn't need to worry about other threads tampering with the variables. That's the reason the second example copies the number of iterations into a local variable before running the loop, and copies the result out of a local variable into a class variable right at the very end. This may or may not make a significant difference to your test (on the current CLR), but I believe it's good practice anyway - although you need to bear this in mind when considering using the results in a real application!

That's it for this post - I'll be back on Friday with my last post before the New Year (assuming I manage to stay away from my computer during our annual, end-of-year office shut-down).

December 19, 2007 in Visual Studio | Permalink | Comments (3) | TrackBack

Getting AutoCAD's current directory from a .NET application

This question came in via a comment from Danny Polkinhorn on this post:

After AutoCAD has launched, I often have a need to determine the "Start In" directory specified in the shortcut (Right-click AutoCAD shortcut, Properties, Start in). Is there a way to determine what that directory is? I'm thinking I can use the path of the active drawing (either Drawing1 or the drawing they double-clicked on). Is that a fool-proof way?

This is an interesting topic, and raises the questions of what that the "Start In" setting is used for and how it can be modified.

The "Start In" property in a Windows shortcut specifies the initial "current directory" for a process. The concept of a current or working directory is common across the various Operating Systems I've worked with: processes have a current directory which is the default file-system folder used for certain operations (such as opening a drawing inside AutoCAD). It is also used by Windows when attempting to locate DLLs to load.

As Danny mentions, it is possible to specify the current directory for a process when it is launched: common ways are to edit the "Start In" property in the shortcut used to launch the application, to change the current directory (using CHDIR or CD) in a command-prompt window and launch the executable from there, or even to specify the working directory for an application to be launched from the Visual Studio debugger. And while the current directory is generally specified as the process starts, it can also change during execution.

AutoCAD 2008's default "Start In" location is "C:\Program Files\Autodesk\AutoCAD 2008\UserDataCache\". Just to verify the process of querying it from an application, I copied the standard shortcut and modified the property to "c:\temp":

Shortcut_properties

I know of two ways to query this property:

System.Environment.CurrentDirectory

System.IO.Directory.GetCurrentDirectory()

I created a simple command, CURDIR, to query this property and print the results.

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;


namespace CurrentDirectory

{

  public class Commands

  {

    [CommandMethod("CURDIR")]

    public void GetCurrentDirectory()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;


      ed.WriteMessage(

        "\nSystem.Environment.CurrentDirectory: "

        + System.Environment.CurrentDirectory

        + "\nSystem.IO.Directory.GetCurrentDirectory(): "

        + System.IO.Directory.GetCurrentDirectory()

      );

    }

  }

}

This code works very well, but it may not return the results expected, depending on when the command is executed and - in particular - how the module is loaded. If we use demand-loading to load your module, it will generally returns the results we expect, assuming it runs early enough. Here's what's returned by the CURDIR command once it has demand-loaded our module in the instance of AutoCAD created by the modified shortcut:

Command: curdir


System.Environment.CurrentDirectory: C:\temp

System.IO.Directory.GetCurrentDirectory(): C:\temp

If, on the other hand, we use the NETLOAD command to load our module, we get the directory from which it was loaded:

Command: netload


Command: curdir


System.Environment.CurrentDirectory: C:\Program Files\Autodesk\ObjectARX

2008\samples\dotNet\Prompts\bin\Debug

System.IO.Directory.GetCurrentDirectory(): C:\Program Files\Autodesk\ObjectARX

2008\samples\dotNet\Prompts\bin\Debug

It took me some time to realise what was happening: it appears that the standard "file navigation" dialog inside AutoCAD - whether it's being used to open a drawing, load an application or perform an action on another type of file - sets the current directory to its most recent location, the first time it is run and closed. So even if you escape from the OPEN command, the location to which you had browsed before cancelling becomes the new current directory.

Whether this is intuitive or expected isn't really for me to say (although I can understand why people might have an opinion). The moral of the story is that you cannot reliably determine the "Start In" folder for AutoCAD (or any other complex application, in reality), unless you ask it very early on in its execution. Code hosted in the process can modify this setting as it feels it needs to.

While not 100% reliable - there are no guarantees some other code has not modified the property beforehand - your best chance at identifying the "Start In" location is to have an application module load on AutoCAD start-up and query it there.

December 17, 2007 in AutoCAD, AutoCAD .NET | Permalink | Comments (9) | TrackBack

Launching AutoCAD from a .NET application

This topic has been raised a few times, and Adam Nagy, from our DevTech EMEA team, sent a technical response recently with code that I decided to use as the basis for this post.

Developers typically want to either integrate functionality into AutoCAD (using its plug-in architecture to add commands, user-interface elements, objects, etc.), or to drive it, automating common tasks. Clearly the line can get blurred between these two areas, but today I’m going to focus on the second category.

To help with later explanations, I’d like to introduce two types of application interaction:

Out-of-process

In this situation we have two separate executables trying to communicate. Imagine you have an EXE and you want to drive AutoCAD. You need to find some way to launch AutoCAD and communicate with it – typically using COM or, before that, DDE. The communication is, by definition, done via Inter-Process Communication (IPC), which is a very inefficient way to send large chunks of data. This is why old ADS and external VB applications ran (or run) so slowly.

In-process

When your code is packaged inside a DLL (whether an ActiveX DLL defined in VB6, an ObjectARX module or a .NET assembly), the communication with the host process (AutoCAD) is much more efficient – data structures can be passed by pointer or reference rather than squirting the information through the inefficient marshalling of IPC.

Most of AutoCAD’s current APIs are designed to be used “in-process” – this includes LISP, ObjectARX and AutoCAD’s .NET API. Given the availability of .NET Remoting, people often hope or expect AutoCAD to be drivable via .NET “out-of-process”, but this is not how the managed API was designed: it is really a wrapper on top of ObjectARX, whose performance is based on direct access to objects via pointers, and this simply does not extend to work across process boundaries. One of the great features of COM Automation is that it was designed to work either out-of-process (from an external EXE) or in-process (from VBA or by calling GetInterfaceObject() to load a VB6 ActiveX DLL). This is still the best way to drive AutoCAD from an external executable.

I generally recommend not trying to pass too much information across the process boundary: if you need to drive AutoCAD from an external executable, simply launch it (or connect to a running instance, if that’s an option) and then load an in-process module to do the heavy lifting from within AutoCAD’s process space.

The following code shows how to do just that, using C#. It tries to connect to a running instance of AutoCAD (this is optional – you can easily adjust the code only to launch AutoCAD), and failing that launches it. Once there’s a running instance we make it visible and run a custom command. My recommendation is to set up demand-loading for your application – either to load it on AutoCAD startup or when a command is invoked – and then to run a command defined in your module.

You will need to add a reference to the “AutoCAD Type Library” from your application, as well as including these assembly references:

using System;

using System.Runtime.InteropServices;

using Autodesk.AutoCAD.Interop;

This C# code can be added to an “on-click” event handler for a button (for example) or another function that makes sense in the context of your application.

  // "AutoCAD.Application.17" uses 2007 or 2008,

  //  whichever was most recently run


  // "AutoCAD.Application.17.1" uses 2008, specifically


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


  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)

  {

    // By the time this is reached AutoCAD is fully

    // functional and can be interacted with through code

    acApp.Visible = true;

    acApp.ActiveDocument.SendCommand("_MYCOMMAND ");

  }

December 14, 2007 in AutoCAD, AutoCAD .NET | Permalink | Comments (8) | TrackBack

Creating an AutoCAD Wipeout using .NET

This topic was suggested via a comment from Dale Bartlett on this post.

In AutoCAD 2008 an API was exposed for the Wipeout object. For those of you who aren't familiar with Wipeouts: this handy object was originally developed as part of the AutoCAD Express Tools, and has since been integrated into core AutoCAD. The implementation uses a raster image of the same colour as the drawing canvas to "wipe out" the graphics behind it (assuming it's nearer the front in terms of draw order relative to the entities being masked). This is the main reason the Wipeout class is derived from RasterImage in the managed API.

The RasterImage-based implementation was chosen at a time when True Color support was not available through AutoCAD's graphics interface (a.k.a. AcGi for the ObjectARX coders reading this). Since then it has been altogether possible to implement Wipeouts without using RasterImage: you can simply use the AcGi protocol to create the same effect. But that's history - once the Wipeout class became widely used it was unfeasible to change the implementation and introduce an incompatibility. On a design note: if the raster had been contained rather that derived from, the choice would have been simpler (an implementation detail, rather than something affecting the class hierarchy in AutoCAD). The design choice was probably due to the capabilities of the ObjectARX API at the time the object was originally developed - containment was inherently more complicated that derivation, back then. Anyway, I digress.

The below C# code shows how to create a Wipeout based on an array of points - in this case we simply make it square, from 0,0 to 100,100. The tricky part is to make sure we close the wipeout manually by specifying the first vertex again as the last point in the list - something Dale and I discovered independently when researching this.

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;


namespace WipeoutApplication

{

  public class Commands

  {

    [CommandMethod("CW")]

    public void CreateWipeout()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;


      Transaction tr =

        db.TransactionManager.StartTransaction();

      using (tr)

      {

        BlockTable bt =

          (BlockTable)tr.GetObject(

            db.BlockTableId,

            OpenMode.ForRead,

            false

          );

        BlockTableRecord btr =

          (BlockTableRecord)tr.GetObject(

            bt[BlockTableRecord.ModelSpace],

            OpenMode.ForWrite,

            false

          );


        Point2dCollection pts =

          new Point2dCollection(5);


        pts.Add(new Point2d(0.0, 0.0));

        pts.Add(new Point2d(100.0, 0.0));

        pts.Add(new Point2d(100.0, 100.0));

        pts.Add(new Point2d(0.0, 100.0));

        pts.Add(new Point2d(0.0, 0.0));


        Wipeout wo = new Wipeout();

        wo.SetDatabaseDefaults(db);

        wo.SetFrom(pts, new Vector3d(0.0, 0.0, 0.1));


        btr.AppendEntity(wo);

        tr.AddNewlyCreatedDBObject(wo, true);

        tr.Commit();

      }

    }

  }

}

December 12, 2007 in AutoCAD, AutoCAD .NET | Permalink | Comments (3) | TrackBack

Getting the names of the colors in an AutoCAD color-book using .NET

The last post got me thinking about how to get the names of all the colours that are contained in a particular color-book inside AutoCAD (the last post also contains the explanation for my using both "color" and "colour" in the same sentence, in case that bothers anyone :-).

Color-books are stored in .acb files: these files are essentially XML files with the RGB values encoded to prevent people from editing them and polluting the colour definitions on a particular system. So while the RGB information is not directly useful, it is very possible to iterate through these files and extract the names of the colours contained in a particular color-book.

This was also an excuse for me to look at the XmlReader class, to see how I might use that. It turns out that you can't instanciate an XmlReader directly - it's an abstract class. You need to use one of its concrete children, such as XmlTextReader or XmlNodeReader. XmlNodeReader gives you tree-like access to the XML content (such as you get when using the DOM to read an XML file), while the XmlTextReader is a forward-only reader which allows you to (for all intents and purposes) stream in XML content such as you might when using SAX. To understand how XmlTextReader differs from SAX, see this brief comparison.

I have historically used DOM to read XML files that need to maintained in their entirety (as they have internal references, etc.) and SAX when this was less of a concern (such as when iterating through to harvest certain data). SAX was always quicker, but you had to maintain more state information if the content was inter-related.

In this case I would therefore have chosen SAX over DOM, so in the .NET world this means XmlTextReader is the class to use.

One other quick comment on the implementation: the code uses HostApplicationServices.FindFile() to find the location of the particular .acb file we’re interested in (typically inside “Support/Color” beneath the AutoCAD install folder).

Here's the C# code:

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using System;

using System.Xml;


namespace ColorBookApplication

{

  public class Commands

  {

    [CommandMethod("LC")]

    static public void ListRalColors()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;


      string loc =

        HostApplicationServices.Current.FindFile(

          "ral classic.acb",

          db,

          FindFileHint.Default

        );


      XmlTextReader xr =

        new XmlTextReader(loc);


      while (xr.Read())

      {

        xr.MoveToContent();

        if (xr.NodeType == XmlNodeType.Element &&

            xr.LocalName == "colorName")

        {

          if (xr.Read())

          {

            if (xr.HasValue)

            {

              ed.WriteMessage("\n" + xr.Value);

            }

          }

        }

      }

      xr.Close();

    }

  }

}

And here's what happens when we run the LC command:

Command: lc


RAL 1000

RAL 1001

RAL 1002


... entries deleted...


RAL 9016

RAL 9017

RAL 9018

December 10, 2007 in AutoCAD, AutoCAD .NET | Permalink | Comments (8) | TrackBack

Using a color-book entry to set the colour of an AutoCAD entity using .NET

Another quick post, as I'm just about to head back home after a long week in Boston. This post comes from a technical solution provided by Sreekar Devatha, from DevTech India.

I won't go into detail regarding the following code, but it should be fairly clear what's going on: from a particular colour-name it looks up a colour from a color-book (yes, I know I've used "colour" and "color" in the same sentence, but "color-book" is an AutoCAD term and it's hard for me to drop my u's when I don't have to, even after having lived in the US for a number of years... :-)

Once it gets the colour, it then uses it to set the colour of an entity the user selects.

Here's the C# code:

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Colors;

using System;

using System.ComponentModel;

using System.Globalization;


namespace ColorBookApplication

{

  public class Commands

  {

    [CommandMethod("SC")]

    static public void SetColorToRal()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;


      ITypeDescriptorContext typeDes = null;

      CultureInfo cult = null;

      ColorConverter conv = new ColorConverter();

      String str = "RAL CLASSIC$RAL 1000";


      Color col = null;


      try

      {

        // Retrieve color from color-book

        col =

          conv.ConvertFrom(typeDes, cult, str) as Color;

      }

      catch (System.Exception ex)

      {

        ed.WriteMessage("Exception: " + ex.Message);

      }


      if (col != null)

      {

        // Select an entity to set the retrieved color

        PromptEntityResult per =

          ed.GetEntity("Select an entity: ");


        if (per.Status == PromptStatus.OK)

        {

          Transaction tr =

            db.TransactionManager.StartTransaction();

          using (tr)

          {

            try

            {

              Entity ent =

                (Entity)tr.GetObject(

                  per.ObjectId,

                  OpenMode.ForWrite

                );

              // Set the color

              ent.Color = col;

              tr.Commit();

            }

            catch (Autodesk.AutoCAD.Runtime.Exception ex)

            {

              ed.WriteMessage("Exception: " + ex.Message);

              tr.Abort();

            }

          }

        }

      }

    }

  }

}

December 7, 2007 in AutoCAD, AutoCAD .NET | Permalink | Comments (0) | TrackBack

Autodesk Freewheel developer contest

As Scott Sheppard has posted on It's Alive in the Lab, a developer contest for the most innovative Autodesk Freewheel application was launched at AU:

http://freewheel.autodesk.com/contest

To get a head-start, you might want to check out the source code I've posted here.

Good luck! :-)

December 4, 2007 in Freewheel | Permalink | Comments (1) | TrackBack