雪山飞狐_lzh 发表于 2009-5-19 18:07:00

十一、
August 08, 2008
Catching exceptions thrown from dialogs inside AutoCAD using .NET
Another juicy tidbit from an internal Q&A session.
The question...
How can I throw an exception from within a modal form on mine inside AutoCAD, and catch it in the calling command? I have tried try/catch, but nothing seems to work.
Here's my command definition:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using System.Windows.Forms;
using MyApplication;
using acApp =
Autodesk.AutoCAD.ApplicationServices.Application;
namespace CatchMeIfYouCan
{
public class Commands
{
   
    static public void CatchDialogException()
    {
      try
      {
      MyForm form = new MyForm();
      DialogResult res =
          acApp.ShowModalDialog(form);
      }
      catch (System.Exception ex)
      {
      MessageBox.Show(
          "Caught using catch: " +
          ex.Message,
          "Exception"
      );
      }
    }
}
}
And here's the code that throws an exception from behind button inside my form:
using System;
using System.Windows.Forms;
namespace MyApplication
{
public partial class MyForm : Form
{
    public MyForm()
    {
      InitializeComponent();
    }
    private void button1_Click(
      object sender,
      EventArgs e
    )
    {
      throw new Exception(
      "Something bad happened."
      );
    }
}
}
The answer, once again, came from our AutoCAD Engineering team, in this case from one of our API Test Engineers based in Singapore...
I was able to catch the exception using the ThreadExceptionEventHandler event. Here is your modified command:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using System.Windows.Forms;
using System.Threading;
using MyApplication;
using acApp =
Autodesk.AutoCAD.ApplicationServices.Application;
using sysApp =
System.Windows.Forms.Application;
namespace CatchMeIfYouCan
{
public class Commands
{
   
    static public void CatchDialogException()
    {
      try
      {
      sysApp.ThreadException +=
          new ThreadExceptionEventHandler(
            delegate(
            object o,
            ThreadExceptionEventArgs args
            )
            {
            MessageBox.Show(
                "Caught using event: " +
                args.Exception.Message,
                "Exception"
            );
            }
          );
      MyForm form = new MyForm();
      DialogResult res =
          acApp.ShowModalDialog(form);
      }
      catch (System.Exception ex)
      {
      MessageBox.Show(
          "Caught using catch: " +
          ex.Message,
          "Exception"
      );
      }
    }
}
}
I gave this a try, myself, and saw that when we click the button on the dialog shown by the CATCH command, our event handler picks up the exception and displays it via a MessageBox:

One thing to note: when running this from the debugger, Visual Studio will break, telling us that there's an exception that has gone unhandled:

This can be ignored - and probably disabled inside Visual Studio - but it could be annoying, if you use this technique throughout your code. Perhaps someone who's implemented this approach - or another that works for them - can add a comment on how they handle this scenario?

雪山飞狐_lzh 发表于 2009-5-19 18:11:00

十二、
September 05, 2008
No muttering at the back! Reducing the background noise when sending commands to AutoCAD
Since posting three different options for Zooming to a Window or Entity inside AutoCAD, I've had a few discussions with a developer on how best to implement this cleanly. The requirement is to change the AutoCAD view via a smooth view transition (currently not exposed via any kind of view-modification API, only via the ZOOM command), but also to hide the fact we're sending commands to the command-line to do so.
While we were discussing, I remembered an old friend, the NOMUTT system variable, which allows almost all command-line noise to be filtered out - even the "Command:" prompt disappears. While this technique is useful for this specific situation, it's also potentially very useful for many other instances where sending commands are currently the best available method of accessing particular functionality inside AutoCAD.
A word of caution: setting NOMUTT to 1 can lead to severe user-disorientation. Please remember to set the variable back to 0, afterwards!
Here's the modified C# code showing this technique - this time using SendStringToExecute() from the .NET API, rather than the COM API's synchronous SendCommand():
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
namespace ZoomSmoothlyAndQuietly
{
public class Commands
{
    // Zoom to a window specified by the user
   
    static public void LoudZoomWindow()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      Editor ed = doc.Editor;
      Point2d min, max;
      if (GetWindowForZoom(ed, out min, out max))
      ZoomWin(ed, min, max, false);
    }
   
    static public void QuietZoomWindow()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      Editor ed = doc.Editor;
      Point2d min, max;
      if (GetWindowForZoom(ed, out min, out max))
      ZoomWin(ed, min, max, true);
    }
    // Get the coordinates for a zoom
    static private bool GetWindowForZoom(
      Editor ed, out Point2d min, out Point2d max
    )
    {
      min = new Point2d();
      max = new Point2d();
      PromptPointOptions ppo =
      new PromptPointOptions(
          "\nSpecify first corner:"
      );
      PromptPointResult ppr =
      ed.GetPoint(ppo);
      if (ppr.Status != PromptStatus.OK)
      return false;
      min =
      new Point2d(ppr.Value.X, ppr.Value.Y);
      PromptCornerOptions pco =
      new PromptCornerOptions(
          "\nSpecify opposite corner: ",
          ppr.Value
      );
      ppr = ed.GetCorner(pco);
      if (ppr.Status != PromptStatus.OK)
      return false;
      max =
      new Point2d(ppr.Value.X, ppr.Value.Y) ;
      return true;
    }
    // Zoom by sending a command
    private static void ZoomWin(
      Editor ed, Point2d min, Point2d max, bool quietly
    )
    {
      string lower =
      min.ToString().Substring(
          1,
          min.ToString().Length - 2
      );
      string upper =
      max.ToString().Substring(
          1,
          max.ToString().Length - 2
      );
      string cmd =
      "_.ZOOM _W " + lower + " " + upper + " ";
      if (quietly)
      {
      // Get the old value of NOMUTT
      object nomutt =
          Application.GetSystemVariable("NOMUTT");
      // Add the string to reset NOMUTT afterwards
      cmd += "_NOMUTT " + nomutt.ToString() + " ";
      // Set NOMUTT to 1, reducing cmd-line noise
      Application.SetSystemVariable("NOMUTT", 1);
      }
      // Send the command(s)
      ed.Document.SendStringToExecute(
      cmd, true, false, !quietly
      );
    }
}
}
Here's what happens when we run the "loud" command (LZW), and then the "quiet" one (QZW):
Command: LZW
Specify first corner:
Specify opposite corner:
Command: _.ZOOM
Specify corner of window, enter a scale factor (nX or nXP), or
<real time>: _W
Specify first corner: 13.1961936276982,12.972925917324 Specify opposite corner:
30.6221132095147,0.482638801189623
Command: QZW
Specify first corner:
Specify opposite corner:
Command:
Both versions of the command deliver the results in terms of smooth view transitions, but the quiet version reduces the command-line clutter by a) passing a flag to SendStringToExecute() to stop the command from being echoed and b) setting the NOMUTT system variable to prevent the command "muttering" from being echoed. It uses a simple trick to reset the system variable afterwards by appending "_NOMUTT 0 " to the string to be executed (which we don't see, as NOMUTT is still set to 1 :-). Assuming the ZOOM command terminates correctly, the NOMUTT system variable should be reset: there is a slight risk that something causes ZOOM to fail, at which point it might be worth checking NOMUTT from time to time elsewhere in your app: as mentioned earlier, having NOMUTT set to 1 can be very disconcerting for the user. I should, in fact, probably just force NOMUTT to 0 after the ZOOM (if you check the the above code, you'll see we reset it to the prior value, which is generally a good technique when modifying system variables as they retain the value previously chosen by the user). Anyway - I'll leave the final choice up to you, but do be aware of the risk.
Update:
Undo is a problem with this implementation - havin NOMUTT as a separate command leaves it at risk of being undone by the user (leaving the system in a scary, silent state). This post presents an enhanced approach which provides much better support for undo.

雪山飞狐_lzh 发表于 2009-5-19 18:13:00

September 15, 2008
More quiet command-calling: adding an inspection dimension inside AutoCAD using .NET
I'm still a little frazzled after transcribing the 18,000 word interview with John Walker (and largely with two fingers - at such times the fact that I've never learned to touch-type is a significant cause of frustration, as you might imagine). I'm also attending meetings all this coming week, so I've gone for the cheap option, once again, of dipping into my magic folder of code generated and provided by my team.
The technique for this one came from a response sent out by Philippe Leefsma, from DevTech EMEA, but he did mention a colleague helped him by suggesting the technique. So thanks to Philippe and our anonymous colleague for the initial idea of calling -DIMINSPECT.
The problem solved by Philippe's code is that no API is exposed via .NET to enable the "inspection" capability of dimensions inside AutoCAD. The protocol is there in ObjectARX, in the AcDbDimension class, but this has - at the time of writing - not been exposed viathe managed API. One option would be to create a wrapper or mixed-mode module to call through to unmanaged C++, but the following approach simply calls through to the -DIMINSPECT command (the command-line version of DIMINSPECT) to set the inspection parameters.
I've integrated - and extended - the technique shown in this previous post to send the command as quietly as possible. One problem I realised with the previous implementation is that UNDO might leave the user in a problematic state - with the NOMUTT variable set to 1. This modified approach does a couple of things differently:
Rather than using the NOMUTT command to set the system variable, it launches another custom command, FINISH_COMMAND
This command has been registered with the NoUndoMarker command-flag, to make it invisible to the undo mechanism (well, at least in terms of automatic undo)
It sets the NOMUTT variable to 0
It should be possible to share this command across others that have the need to call commands quietly
It uses the COM API to create an undo group around the operations we want to consider one "command"
The implementation related to the "quiet command calling" technique is wrapped up in a code region to make it easy to hide
One remaining - and, so far, unavoidable - piece of noise on the command-line is due to our use of the (handent) function: it echoes entity name of the selected dimension.
Here's the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Interop;
namespace InspectDimension
{
public class Commands
{
   
    static public void InspectDim()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      Database db = doc.Database;
      Editor ed = doc.Editor;
      PromptEntityOptions peo =
      new PromptEntityOptions("\nSelect a dimension: ");
      peo.SetRejectMessage(
      "\nEntity must be a dimension."
      );
      peo.AddAllowedClass(typeof(Dimension), false);
      PromptEntityResult per = ed.GetEntity(peo);
      if (per.Status != PromptStatus.OK)
      return;
      Transaction tr =
      db.TransactionManager.StartTransaction();
      using (tr)
      {
      Dimension dim =
          tr.GetObject(per.ObjectId, OpenMode.ForRead)
            as Dimension;
      if (dim != null)
      {
          string shape = "Round";
          string label = "myLabel";
          string rate = "100%";
          string cmd =
            "-DIMINSPECT Add (handent \"" +
            dim.Handle + "\"" + ") \n" +
            shape + "\n" + label + "\n" +
            rate + "\n";
          SendQuietCommand(doc, cmd);
      };
      tr.Commit();
      }
    }
    #region QuietCommandCalling
    const string kFinishCmd = "FINISH_COMMAND";
    private static void SendQuietCommand(
      Document doc,
      string cmd
    )
    {
      // Get the old value of NOMUTT
      object nomutt =
      Application.GetSystemVariable("NOMUTT");
      // Add the string to reset NOMUTT afterwards
      AcadDocument oDoc =
      (AcadDocument)doc.AcadDocument;
      oDoc.StartUndoMark();
      cmd += "_" + kFinishCmd + " ";
      // Set NOMUTT to 1, reducing cmd-line noise
      Application.SetSystemVariable("NOMUTT", 1);
      doc.SendStringToExecute(cmd, true, false, false);
    }
   
    static public void FinishCommand()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      AcadDocument oDoc =
      (AcadDocument)doc.AcadDocument;
      oDoc.EndUndoMark();
      Application.SetSystemVariable("NOMUTT", 0);
    }
    #endregion
}
}
Here are the results of running the INSP command and selecting a dimension object. First the command-line output:
Command: INSP
Select a dimension: <Entity name: -401f99f0>
Command:And now the graphics, before and after calling INSP and selecting the dimension:


In the end the post didn't turn out to be quite as quick to write as expected, but anyway - so it goes. I'm now halfway from Zürich to Washington, D.C., and had the time to kill. I probably won't have the luxury when I'm preparing my post for the middle of the week, unless I end up suffering from jet-lag. :-)

雪山飞狐_lzh 发表于 2009-5-19 18:19:00

十三、防止的.NET模块被加载
September 08, 2008
Preventing a .NET module from being loaded by AutoCAD
This is an interesting one that came up recently during an internal discussion:
During my module's Initialize() function, I want to decide that the module should not actually be loaded. How can I accomplish that?
The answer is surprisingly simple: if you throw an exception during the function, AutoCAD's NETLOAD mechanism will stop loading the application.
For an example, see this C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
namespace PreventLoad
{
public class Commands
: IExtensionApplication
{
    public void Initialize()
    {
      // This will prevent the application from loading
      throw new Exception();
    }
    public void Terminate(){}
   
    static public void TestCommand()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      doc.Editor.WriteMessage(
      "\nThis should not get called."
      );
    }
}
}
Here's what happens when we attempt to load the application and then run the TEST command:
Command: FILEDIA
Enter new value for FILEDIA <1>: 0
Command: NETLOAD
Assembly file name: c:\MyApplication.dll
Command: TEST
Unknown command "TEST".Press F1 for help.

雪山飞狐_lzh 发表于 2009-5-19 18:23:00

十四、使用代码载入.NET的模块
September 17, 2008
Loading .NET modules programmatically into AutoCAD
The question of how to perform a "NETLOAD" programmatically has come in a few times. Well, it turns out the answer - provided by someone in our Engineering team - is refreshingly simple.
The NETLOAD command actually has a bare-bones implementation: the hard work of parsing the metadata defining commands etc. is done from an AppDomain.AssemblyLoad event-handler. To recreate the NETLOAD command, all you need to do is call Assembly.LoadFrom(), passing in the path to your assembly.
Here's some C# code to demonstrate this:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Reflection;
namespace LoadModule
{
public class Commands
{
   
    static public void MyNetLoad()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      Editor ed = doc.Editor;
      PromptStringOptions pso =
      new PromptStringOptions(
          "\nEnter the path of the module to load: "
      );
      pso.AllowSpaces = true;
      PromptResult pr = ed.GetString(pso);
      if (pr.Status != PromptStatus.OK)
      return;
      try
      {
      Assembly.LoadFrom(pr.StringResult);
      }
      catch(System.Exception ex)
      {
      ed.WriteMessage(
          "\nCannot load {0}: {1}",
          pr.StringResult,
          ex.Message
      );
      }
    }
}
}
Incidentally, my preferred way to manage this is to enable demand loading for your assemblies, as per this previous post. But there are certainly situations where the above technique will prove useful.
In the next post: on Friday you should see the second installment of the interview with John Walker...

雪山飞狐_lzh 发表于 2009-5-19 18:54:00

十五、自定义AutoCAD的命令显示在上下文菜单中
November 05, 2008
Displaying a context menu during a custom AutoCAD command using .NET
Some of you may have stumbled across these previous posts, which show how to add custom context menu items for specific object types and to the default AutoCAD context menu. There is a third way to create and display context menus inside AutoCAD, and this approach may prove useful to those of you who wish to display context menus during particular custom commands.One word of caution: I've been told that this technique does not currently work for transparent commands, so if your command needs to be called transparently then this may not be the approach for you (you should investigate asking for keywords, instead, as this should work without problem in this context).
Here's the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using System;
namespace CommandContextMenu
{
public class Commands
{
    public class MyContextMenu : ContextMenuExtension
    {
      public MyContextMenu()
      {
      this.Title = "Command context menu";
      MenuItem mi = new MenuItem("Item One");
      mi.Click +=
          new EventHandler(Commands.OnClick);
      this.MenuItems.Add(mi);
      mi = new MenuItem("Item Two");
      mi.Click +=
          new EventHandler(Commands.OnClick);
      this.MenuItems.Add(mi);
      MenuItem smi = new MenuItem("Sub Item One");
      smi.Click +=
          new EventHandler(Commands.OnClick);
      this.MenuItems.Add(smi);
      }
    };
    [CommandMethod(
      "mygroup",
      "mycmd",
      null,
      CommandFlags.Modal,
      typeof(MyContextMenu)
    )]
    public static void MyCommand()
    {
      Editor ed =
      Application.DocumentManager.MdiActiveDocument.Editor;
      ed.GetPoint("\nRight-click before selecting a point:");
    }
    static void OnClick(object sender, EventArgs e)
    {
      Editor ed =
      Application.DocumentManager.MdiActiveDocument.Editor;
      ed.WriteMessage("\nA context menu item was selected.");
    }
}
}
A couple of points about getting this working: on my system I used AutoCAD's OPTIONS command to make sure the right-click menu gets displayed after a certain delay (I enabled the option from the "User Preferences" -> "Right-Click Customization" and then changed the longer click duration to 100 milliseconds, to save a little time. :-)

The other option for enabling command menus is the SHORTCUTMENU system variable (the documentation inside AutoCAD will tell you about the various values).
Then, once inside the MYCMD command, I was able to right-click and see my custom menu:

From the command-line, we can see that our callback was called successfully:
Command:MYCMD
Right-click before selecting a point:
A context menu item was selected.If you wish to have separate callbacks for your various items, it's simply a matter of defining separate functions and passing them in as the Click event handler instead of Command.OnClick.

雪山飞狐_lzh 发表于 2009-5-19 19:09:00

十六、从外部引用导入层
May 18, 2009
Importing AutoCAD layers from xrefs using .NET
This post extends the last one which looked at a basic implementation to allow AutoCAD’s standard OFFSET command to work on the contents of external references. I haven’t flagged the specific changes, but the old code (which is almost identical to that in the last post) starts with the Initialize() function.
The previous example created geometry on a temporary layer that exists only as long as the source xref is attached: detaching the xref generally caused dangling layer references. This post evolves the approach and provides a choice to the user (via the XOFFSETLAYER command): to either create the geometry on the current layer (the default behaviour) or to clone across the layers, plus their various linetypes etc., needed for the geometry to survive independently of their originating xref.
I decided to put the code to copy across the layers in a separate command called XOFFSETCPLAYS, which has the advantage of being callable separately from our OFFSET routine (and is generally a bit safer – performing significant operations on AutoCAD databases from event handlers can lead to problems, depending on what exactly you’re doing). The command gets called using SendStringToExecute() so that it gets executed up after control has returned to the AutoCAD application.
The copying of symbol table records from xrefs is a little involved, but I’ve don what I can to comment the approach below. Using Database.wblockCloneObjects() is the cleanest approach, but there is work involved to load the xrefed DWGs into their own Database objects before using this.
Here’s the updated C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;

namespace XrefOffset
{
public class XrefOffsetApplication : IExtensionApplication
{
    // Maintain a list of temporary objects that require removal

    ObjectIdCollection _ids;
    static bool _placeOnCurrentLayer = true;

    public XrefOffsetApplication()
    {
      _ids = new ObjectIdCollection();
    }

   
    static public void XrefOffsetLayer()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      Database db = doc.Database;
      Editor ed = doc.Editor;

      PromptKeywordOptions pko =
      new PromptKeywordOptions(
          "\nEnter layer option for offset objects"
      );
      const string option1 = "Current";
      const string option2 = "Source";

      pko.AllowNone = true;
      pko.Keywords.Add(option1);
      pko.Keywords.Add(option2);
      pko.Keywords.Default =
      (_placeOnCurrentLayer ? option1 : option2);

      PromptResult pkr =
      ed.GetKeywords(pko);

      if (pkr.Status == PromptStatus.OK)
      _placeOnCurrentLayer =
          (pkr.StringResult == option1);
    }

   
    static public void XrefOffsetCopyLayers()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      Database db = doc.Database;
      Editor ed = doc.Editor;

      Transaction tr =
      doc.TransactionManager.StartTransaction();
      using (tr)
      {
      BlockTableRecord btr =
          (BlockTableRecord)tr.GetObject(
            db.CurrentSpaceId,
            OpenMode.ForRead
          );

      // We will collect the layers used by the various entities

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

      // And store a list of the entities to come back and update

      ObjectIdCollection entsToUpdate =
          new ObjectIdCollection();

      // Loop through the contents of the active space, and look
      // for entities that are on dependent layers (i.e. ones
      // that come from attached xrefs)

      foreach (ObjectId entId in btr)
      {
          Entity ent =
            (Entity)tr.GetObject(entId, OpenMode.ForRead);

          // Check the dependent status of the entity's layer

          LayerTableRecord ltr =
            (LayerTableRecord)tr.GetObject(
            ent.LayerId,
            OpenMode.ForRead
            );

          if (ltr.IsDependent && !(ent is BlockReference))
          {
            // Add it to our list and flag the entity for updating

            string layName = ltr.Name;
            if (!layerNames.Contains(layName))
            {
            layerNames.Add(layName);
            }
            entsToUpdate.Add(ent.ObjectId);
          }
      }
      // Sorting the list will allow us to minimise the number
      // of external drawings we need to load (many layers
      // will be from the same xref)

      layerNames.Sort();

      // Get the xref graph, which allows us to get at the
      // names of the xrefed drawings more easily

      XrefGraph xg = db.GetHostDwgXrefGraph(false);

      // We're going to store a list of our xrefed databases
      // for later disposal

      List<Database> xrefs =
          new List<Database>();

      // Collect a list of the layers we want to clone across

      ObjectIdCollection laysToClone =
          new ObjectIdCollection();

      // Loop through the list of layers, only loading xrefs
      // in when they haven't been already

      string currentXrefName = "";
      foreach(string layName in layerNames)
      {
          Database xdb = null;

          // Make sure we have our mangled layer name

          if (layName.Contains("|"))
          {
            // Split it up, so we know the xref name
            // and the root layer name

            int sepIdx = layName.IndexOf("|");
            string xrefName =
            layName.Substring(0, sepIdx);
            string rootName =
            layName.Substring(sepIdx + 1);

            // If the xref is the same as the last loaded,
            // this saves us some effort

            if (xrefName == currentXrefName)
            {
            xdb = xrefs;
            }
            else
            {
            // Otherwise we get the node for our xref,
            // so we can get its filename

            XrefGraphNode xgn =
                xg.GetXrefNode(xrefName);

            if (xgn != null)
            {
                // Create an xrefed database, loading our
                // drawing into it

                xdb = new Database(false, true);
                xdb.ReadDwgFile(
                  xgn.Database.Filename,
                  System.IO.FileShare.Read,
                  true,
                  null
                );

                // Add it to the list for later disposal
                // (we do this after the clone operation)

                xrefs.Add(xdb);
            }
            xgn.Dispose();
            }

            if (xdb != null)
            {
            // Start a transaction in our loaded database
            // to get at the layer name

            Transaction tr2 =
                xdb.TransactionManager.StartTransaction();
            using (tr2)
            {
                // Open the layer table

                LayerTable lt2 =
                  (LayerTable)tr2.GetObject(
                  xdb.LayerTableId,
                  OpenMode.ForRead
                  );

                // Add our layer (which we get via its
                // unmangled name) to the list to clone

                if (lt2.Has(rootName))
                {
                  laysToClone.Add(lt2);
                }

                // Committing is cheaper

                tr2.Commit();
            }
            }
          }
      }

      // If we have layers to clone, do so

      if (laysToClone.Count > 0)
      {
          // We use wblockCloneObjects to clone between DWGs

          IdMapping idMap = new IdMapping();
          db.WblockCloneObjects(
            laysToClone,
            db.LayerTableId,
            idMap,
            DuplicateRecordCloning.Ignore,
            false
          );

          // Dispose each of our xrefed databases

          foreach (Database xdb in xrefs)
          {
            xdb.Dispose();
          }

          // Open the resultant layer table, so we can check
          // for the existance of our new layers

          LayerTable lt =
            (LayerTable)tr.GetObject(
            db.LayerTableId,
            OpenMode.ForRead
            );

          // Loop through the entities to update

          foreach (ObjectId id in entsToUpdate)
          {
            // Open them each for write, and then check their
            // current layer

            Entity ent =
            (Entity)tr.GetObject(id, OpenMode.ForWrite);
            LayerTableRecord ltr =
            (LayerTableRecord)tr.GetObject(
                ent.LayerId,
                OpenMode.ForRead
            );

            // We split the name once again (could use a function
            // for this, but hey)

            string layName = ltr.Name;
            int sepIdx = layName.IndexOf("|");
            string xrefName =
            layName.Substring(0, sepIdx);
            string rootName =
            layName.Substring(sepIdx + 1);

            // If we now have the layer in our database, use it

            if (lt.Has(rootName))
            ent.LayerId = lt;
          }
      }
      tr.Commit();
      }
    }

    public void Initialize()
    {
      DocumentCollection dm =
      Application.DocumentManager;

      // Remove any temporary objects at the end of the command

      dm.DocumentLockModeWillChange +=
      delegate(
          object sender, DocumentLockModeWillChangeEventArgs e
      )
      {
          if (_ids.Count > 0)
          {
            Transaction tr =
            e.Document.TransactionManager.StartTransaction();
            using (tr)
            {
            foreach (ObjectId id in _ids)
            {
                DBObject obj =
                  tr.GetObject(id, OpenMode.ForWrite, true);
                obj.Erase();
            }
            tr.Commit();
            }
            _ids.Clear();

            // Launch a command to bring across our layers

            if (!_placeOnCurrentLayer)
            {
            e.Document.SendStringToExecute(
                "_.XOFFSETCPLAYS ", false, false, false
            );
            }
          }
      };

      // When a document is created, make sure we handle the
      // important events it fires

      dm.DocumentCreated +=
      delegate(
          object sender, DocumentCollectionEventArgs e
      )
      {
          e.Document.CommandWillStart +=
            new CommandEventHandler(OnCommandWillStart);
          e.Document.CommandEnded +=
            new CommandEventHandler(OnCommandFinished);
          e.Document.CommandCancelled +=
            new CommandEventHandler(OnCommandFinished);
          e.Document.CommandFailed +=
            new CommandEventHandler(OnCommandFinished);
      };

      // Do the same for any documents existing on application
      // initialization

      foreach (Document doc in dm)
      {
      doc.CommandWillStart +=
          new CommandEventHandler(OnCommandWillStart);
      doc.CommandEnded +=
          new CommandEventHandler(OnCommandFinished);
      doc.CommandCancelled +=
          new CommandEventHandler(OnCommandFinished);
      doc.CommandFailed +=
          new CommandEventHandler(OnCommandFinished);
      }
    }

    // When the OFFSET command starts, let's add our selection
    // manipulating event-handler

    void OnCommandWillStart(object sender, CommandEventArgs e)
    {
      if (e.GlobalCommandName == "OFFSET")
      {
      Document doc = (Document)sender;
      doc.Editor.PromptForEntityEnding +=
          new PromptForEntityEndingEventHandler(
            OnPromptForEntityEnding
          );
      }
    }

    // And when the command ends, remove it

    void OnCommandFinished(object sender, CommandEventArgs e)
    {
      if (e.GlobalCommandName == "OFFSET")
      {
      Document doc = (Document)sender;
      doc.Editor.PromptForEntityEnding -=
          new PromptForEntityEndingEventHandler(
            OnPromptForEntityEnding
          );
      }
    }

    // Here's where the heavy lifting happens...

    void OnPromptForEntityEnding(
      object sender, PromptForEntityEndingEventArgs e
    )
    {
      if (e.Result.Status == PromptStatus.OK)
      {
      Editor ed = sender as Editor;
      ObjectId objId = e.Result.ObjectId;
      Database db = objId.Database;

      Transaction tr =
          db.TransactionManager.StartTransaction();
      using (tr)
      {
          // First get the currently selected object
          // and check whether it's a block reference

          BlockReference br =
            tr.GetObject(objId, OpenMode.ForRead)
            as BlockReference;
          if (br != null)
          {
            // If so, we check whether the block table record
            // to which it refers is actually from an XRef

            ObjectId btrId = br.BlockTableRecord;
            BlockTableRecord btr =
            tr.GetObject(btrId, OpenMode.ForRead)
                as BlockTableRecord;
            if (btr != null)
            {
            if (btr.IsFromExternalReference)
            {
                // If so, then we programmatically select the object
                // underneath the pick-point already used

                PromptNestedEntityOptions pneo =
                  new PromptNestedEntityOptions("");
                pneo.NonInteractivePickPoint =
                  e.Result.PickedPoint;
                pneo.UseNonInteractivePickPoint = true;

                PromptNestedEntityResult pner =
                  ed.GetNestedEntity(pneo);

                if (pner.Status == PromptStatus.OK)
                {
                  try
                  {
                  ObjectId selId = pner.ObjectId;

                  // Let's look at this programmatically-selected
                  // object, to see what it is

                  DBObject obj =
                      tr.GetObject(selId, OpenMode.ForRead);

                  // If it's a polyline vertex, we need to go one
                  // level up to the polyline itself

                  if (obj is PolylineVertex3d || obj is Vertex2d)
                      selId = obj.OwnerId;

                  // We don't want to do anything at all for
                  // textual stuff, let's also make sure we
                  // are dealing with an entity (should always
                  // be the case)

                  if (obj is MText || obj is DBText ||
                      !(obj is Entity))
                      return;

                  // Now let's get the name of the layer, to use
                  // later

                  Entity ent = (Entity)obj;
                  LayerTableRecord ltr =
                      (LayerTableRecord)tr.GetObject(
                        ent.LayerId,
                        OpenMode.ForRead
                      );
                  string layName = ltr.Name;

                  // Clone the selected object

                  object o = ent.Clone();
                  Entity clone = o as Entity;

                  // We need to manipulate the clone to make sure
                  // it works

                  if (clone != null)
                  {
                      // Setting the properties from the block
                      // reference helps certain entities get the
                      // right references (and allows them to be
                      // offset properly)

                      clone.SetPropertiesFrom(br);

                      // But we then need to get the layer
                      // information from the database to set the
                      // right layer (at least) on the new entity

                      if (_placeOnCurrentLayer)
                      {
                        clone.LayerId = db.Clayer;
                      }
                      else
                      {
                        LayerTable lt =
                        (LayerTable)tr.GetObject(
                            db.LayerTableId,
                            OpenMode.ForRead
                        );
                        if (lt.Has(layName))
                        clone.LayerId = lt;
                      }

                      // Now we need to transform the entity for
                      // each of its Xref block reference containers
                      // If we don't do this then entities in nested
                      // Xrefs may end up in the wrong place

                      ObjectId[] conts =
                        pner.GetContainers();
                      foreach (ObjectId contId in conts)
                      {
                        BlockReference cont =
                        tr.GetObject(contId, OpenMode.ForRead)
                            as BlockReference;
                        if (cont != null)
                        clone.TransformBy(cont.BlockTransform);
                      }

                      // Let's add the cloned entity to the current
                      // space

                      BlockTableRecord space =
                        tr.GetObject(
                        db.CurrentSpaceId,
                        OpenMode.ForWrite
                        ) as BlockTableRecord;
                      if (space == null)
                      {
                        clone.Dispose();
                        return;
                      }

                      ObjectId cloneId = space.AppendEntity(clone);
                      tr.AddNewlyCreatedDBObject(clone, true);

                      // Now let's flush the graphics, to help our
                      // clone get displayed

                      tr.TransactionManager.QueueForGraphicsFlush();

                      // And we add our cloned entity to the list
                      // for deletion

                      _ids.Add(cloneId);

                      // Created a non-graphical selection of our
                      // newly created object and replace it with
                      // the selection of the container Xref

                      SelectedObject so =
                        new SelectedObject(
                        cloneId,
                        SelectionMethod.NonGraphical,
                        -1
                        );

                      e.ReplaceSelectedObject(so);
                  }
                  }
                  catch
                  {
                  // A number of innocuous things could go wrong
                  // so let's not worry about the details

                  // In the worst case we are simply not trying
                  // to replace the entity, so OFFSET will just
                  // reject the selected Xref
                  }
                }
            }
            }
          }
          tr.Commit();
      }
      }
    }

    public void Terminate()
    {
    }
}
}
Depending on the mode selected, the results should be comparable with those from the code in the last post.
Here’s a portion of an xref:

Here are the results of using OFFSET on some of the xref’s contents after loading our module (to get these results you would also need to use XOFFSETLAYER to make sure the original layers get copied across):

Update:
I’ve modified the implementation of the XOFFSETLAYER command to be consistent with the prompts in the OFFSET command (under the Layer option – not sure how I missed that one :-). The ideal would have been to access that setting directly, but that doesn’t seem to be possible, as far as I can tell.


雪山飞狐_lzh 发表于 2009-9-7 23:24:00

<p>July 10, 2006<br/>Calling ObjectARX functions from a .NET application</p><p>One of the really compelling features of .NET is its ability to call "legacy" unmanaged C++ APIs. I say "legacy", but we use this facility regularly to call APIs that are far from being considered defunct (the C++ version of ObjectARX is alive and kicking, believe me! :-).</p><p>Autodesk understands that our development partners have invested many years in application development, and can't afford to throw that investment away to support the latest &amp; greatest (and sometimes "flavor of the month") programming technology. For example, over the years we've made sure it was possible to create a VB or VBA user-interface for an existing LISP application or now a .NET user-interface for an ObjectARX application. Sometimes we expose our own interoperability functions to help with this (such as LISP functions to call ActiveX DLLs), and in other cases we advise people on how best to leverage standard Microsoft platform technologies.</p><p>So... how do you call an ObjectARX function from VB.NET? The answer is Platform Invoke (or P/Invoke for short). Microsoft has not exposed the full functionality of the Win32 API through the .NET Framework - just as Autodesk has not exposed all of ObjectARX through AutoCAD's Managed API - but P/Invoke helps you get around this.</p><p>First, some background on what ObjectARX really is, and how P/Invoke can help us.</p><p>ObjectARX is a set of APIs that are exported from DLLs or EXEs. Most exported functions get "decorated" or "mangled" during compilation, unless there is a specific compiler directive not to (this is the case for all the old ADS functions, for instance - they are declared as extern "C" and are therefore not mangled). The compiler assigns a unique name based on the function signature, which makes sense: it is quite legal in C++ to have two functions with the same name, but not with identical arguments and return values. The decorated name includes the full function name inside it, which is why the below technique for finding the correct export works.</p><p>[ Note: this technique works well for C-style functions, or C++ static functions. It will not work on instance members (methods of classes), as it is not possible to instantiate an unmanaged object of the class that defines the class from managed code. If you need to expose a class method to managed code, you will need to write &amp; expose some native C++ code that instantiates the class, calls the method and returns the result. ]</p><p>To demonstrate the procedure we're going to work through the steps needed to call acedGetUserFavoritesDir() from C# and VB.NET. This function is declared in the ObjectARX headers as:</p><p>&nbsp;&nbsp;&nbsp; extern Adesk::Boolean acedGetUserFavoritesDir( ACHAR* szFavoritesDir );</p><p>According to the ObjectARX Reference, "this function provides access to the Windows Favorites directory of the current user."</p><p>Step 1 - Identify the location of the export.</p><p>Fenton Webb, from DevTech EMEA, provided this handy batch file he uses for just this purpose:</p><p>[ Copy and paste this into a file named "findapi.bat", which you then place this into your AutoCAD application folder. You will need to run findapi from a command prompt which knows where to find dumpbin.exe - the Visual Studio Command Prompts created on installing VS will help you with this. ]</p><p>&nbsp;&nbsp;&nbsp; @echo off<br/>&nbsp;&nbsp;&nbsp; if "%1" == "" goto usage<br/>&nbsp;&nbsp;&nbsp; :normal<br/>&nbsp;&nbsp;&nbsp; for %%i IN (*.exe *.dll *.arx *.dbx *.ocx *.ddf) DO dumpbin /exports %%i | findstr "%%i %1"<br/>&nbsp;&nbsp;&nbsp; goto end<br/>&nbsp;&nbsp;&nbsp; :usage<br/>&nbsp;&nbsp;&nbsp; echo findapi "function name"<br/>&nbsp;&nbsp;&nbsp; :end</p><p>You can redirect the output into a text file, of course, for example:</p><p>&nbsp;&nbsp;&nbsp; C:\Program Files\AutoCAD 2007&gt;findapi acedGetUserFavoritesDir &gt; results.txt</p><p>It'll take some time to work, as this batch file chunks through all the DLLs, EXEs, etc. in the AutoCAD application folder to find the results (it doesn't stop when it finds one, either - this enhancement is left as an exercise for the reader ;-).</p><p>Opening the text file will allow you to see where the acedGetUserFavoritesDir() function is exported:</p><p>[ from the results for AutoCAD 2007 ]</p><p>&nbsp;&nbsp;&nbsp; Dump of file acad.exe<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 436&nbsp; 1B0 004B4DC0 <a href="mailto:?acedGetUserFavoritesDir@@YAHPA_W@Z">?acedGetUserFavoritesDir@@YAHPA_W@Z</a></p><p>A word of warning: the decorated names for functions accepting/returning strings changed between AutoCAD 2006 and 2007, because we are now using Unicode for string definition. Here is the previous declaration for 2004/2005/2006 (which was probably valid for as long as the function was defined, back in AutoCAD 2000i, if I recall correctly):</p><p>[ from the results for AutoCAD 2006 ]</p><p>&nbsp;&nbsp;&nbsp; Dump of file acad.exe<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 357&nbsp; 161 00335140 <a href="mailto:?acedGetUserFavoritesDir@@YAHPAD@Z">?acedGetUserFavoritesDir@@YAHPAD@Z</a></p><p>This is simply because the function signature has changed from taking a char* to an ACHAR* (a datatype which now resolves to a "wide" or Unicode string in AutoCAD 2007). A change in the function signature results in a change in the decorated name. This is straightforward enough, but it is worth bearing in mind the potential migration issue - a heavy dependency on decorated function names can lead to substantial migration effort if widespread signature changes are made in a release (as with AutoCAD 2007's support of Unicode).</p><p>Another warning: you will find a number of other functions exported from the various DLLs/EXEs that do not have corresponding declarations in the ObjectARX headers. These functions - while exposed - are not supported. Which means that you may be able to work out how they can be called, but use them at your own risk (which can be substantial). Unsupported APIs are liable to change (or even disappear) without notice.</p><p>Now we've identified where and how the function is exposed, we can create a declaration of this function we can use in our code.</p><p>Step 2 - Declare the function correctly in your code.</p><p>This is going to be slightly different depending on the programming language you're using.</p><p>VB developers will be used to using "Declare" to set-up P/Invoke from their projects. This ends up being translated by the compiler into calls to DllImport, which is also used directly in C#.</p><p>These declarations should be made at the class level (not within an individual function definition).</p><p>VB.NET</p><p>&nbsp;&nbsp;&nbsp; Private Declare Auto Function acedGetUserFavoritesDir Lib "acad.exe" Alias "<a href="mailto:?acedGetUserFavoritesDir@@YAHPA_W@Z">?acedGetUserFavoritesDir@@YAHPA_W@Z</a>" (&lt;MarshalAs(UnmanagedType.LPWStr)&gt; ByVal sDir As StringBuilder) As Boolean</p><p>C#</p><p>&nbsp;&nbsp;&nbsp; <br/>&nbsp;&nbsp;&nbsp; public static extern bool acedGetUserFavoritesDir( StringBuilder sDir);</p><p>Notes:</p><p>&nbsp;&nbsp; 1. It's worth specifying the character set as "Auto" - which is not the default setting. The compiler does a good job of working out whether to use Unicode or ANSI, so it's easiest to trust it to take care of this.<br/>&nbsp;&nbsp; 2. You will need to use the MarshalAs(UnmanagedType.LPWStr) declaration for Unicode string variables in 2007. This is true whether using Strings or StringBuilders.<br/>&nbsp;&nbsp; 3. Use a StringBuilder for an output string parameter, as standard Strings are considered immutable. Strings are fine for input parameters.</p><p><br/>Step 3 - Use the function in your code</p><p>[ I've omited the standard using/import statements, as well as the class &amp; function declarations, to improve readability. ]</p><p>VB.NET</p><p>&nbsp;&nbsp;&nbsp; Dim ed As Editor = Application.DocumentManager.MdiActiveDocument.Editor<br/>&nbsp;&nbsp;&nbsp; Dim sDir As New StringBuilder(256)<br/>&nbsp;&nbsp;&nbsp; Dim bRet As Boolean = acedGetUserFavoritesDir(sDir)<br/>&nbsp;&nbsp;&nbsp; If bRet And sDir.Length &gt; 0 Then<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ed.WriteMessage("Your favorites folder is: " + sDir.ToString)<br/>&nbsp;&nbsp;&nbsp; End If</p><p>C#</p><p>&nbsp;&nbsp;&nbsp; Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;<br/>&nbsp;&nbsp;&nbsp; StringBuilder sDir = new StringBuilder(256);<br/>&nbsp;&nbsp;&nbsp; bool bRet = acedGetUserFavoritesDir(sDir);<br/>&nbsp;&nbsp;&nbsp; if (bRet &amp;&amp; sDir.Length &gt; 0)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ed.WriteMessage("Your favorites folder is: " + sDir.ToString());</p><p>Note: we declare the StringBuilder variable (sDir) as being 256 characters long. AutoCAD expects us to provide a sufficiently long buffer for the data to be copied into it.</p><p>On my system both code snippets resulted in the following being sent to AutoCAD's command-line:</p><p>&nbsp;&nbsp;&nbsp; Your favorites folder is: C:\My Documents\Favorites</p><p>So that's it: you should now be able to call global ObjectARX functions from .NET. This technique can also be used to call your own functions exposed from DLLs... which is one way to allow you to create fancy UIs with .NET and leverage existing C++ code (there are others, such as exposing your own Managed API).</p><p>For additional information on using P/Invoke, particularly with Win32, here is a really great resource.</p><p><a href="http://pinvoke.net/">http://pinvoke.net/</a><br/></p>
页: 1 [2]
查看完整版本: Kean专题(5)—Commands