Moving text in an AutoCAD block using .NET – Part 3

I wasn't expecting to write a third part in this series of posts, but then Samir Bittar went and asked a follow-up question that I felt obliged to look into. Thanks for the suggestion, Samir! 🙂

Samir basically wanted to provide the user with more feedback as they're selecting the nested entity – so that the sub-entity gets highlighted, rather than the full block reference.

This turned out to be quite a tricky scenario to address. The overall approach I used was to use a PointMonitor to perform a non-interactive, nested selection of the geometry beneath the cursor and then highlight the results. The trick was how best to actually perform the selection itself. The options – with their pros and cons – came down to this:

  1. Use Editor.GetNestedEntity() to perform the selection.
    • Nice because you get the square selection cursor displayed.
    • Re-entering the function non-interactively from a PointMonitor causes strange behaviour with the dynamic input tooltip (possibly just one symptom of something that's fundamentally unstable).
  2. Use Editor.GetEntity() to perform the selection.
    • Similar behaviour to 1, without the benefits of selecting the sub-entity.
  3. Use a basic jig and Editor.Drag() to acquire a point.
    • Also allows you to select the square selection cursor.
    • Stops the sub-entity highlighting from working at all.
  4. Use Editor.GetPoint() to get a raw point.
    • Works fine with non-interactive, nested entity selection.
    • You don't get the nice, square selection cursor.
    • You need to disable object snap, to avoid further confusion.

This list may well be missing some options – please do post a comment, if so.

After working through 1-3, I settled on 4. I didn't like the fact you don't get the selection cursor, but it seemed the lesser of the various evils. (I did actually try to replace the system cursor with my own inside the PointMonitor, but this just ended up adding an additional cursor and causing some strange lag effects between it and the standard AutoCAD cross-hairs.)

Here's the C# code that implements this in the MTIBJIGHL command (short for MoveTextInBlockJigHighLight ;-):

using System;

using System.Runtime.InteropServices;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

 

namespace Textformations

{

  public class DisplacementJig2 : EntityJig

  {

    private Point3d _pos;

    private Point3d _loc;

 

    public DisplacementJig2(Entity ent, Point3d basePt)

      : base(ent)

    {

      _loc = basePt;

    }

 

    protected override bool Update()

    {

      var disp = _pos - _loc;

      _loc = _pos;

      var mat = Matrix3d.Displacement(disp);

      Entity.TransformBy(mat);

      return true;

    }

 

    protected override SamplerStatus Sampler(JigPrompts prompts)

    {

      var opts =

        new JigPromptPointOptions("\nSelect displacement");

      opts.BasePoint = _pos;

      opts.UserInputControls =

        UserInputControls.NoZeroResponseAccepted;

 

      var ppr = prompts.AcquirePoint(opts);

      if (_pos == ppr.Value)

        return SamplerStatus.NoChange;

 

      _pos = ppr.Value;

 

      return SamplerStatus.OK;

    }

  }

 

  public class SubEntityHighlighter

  {

    [DllImport(

      "accore.dll",

      CallingConvention = CallingConvention.Cdecl

    )]

    private static extern int acedRedraw(

      ref ads_name name, int mode

    );

    [DllImport(

      "acdb19.dll",

      CallingConvention=CallingConvention.Cdecl,

      EntryPoint=

        "?acdbGetAdsName@@YA?AW4ErrorStatus@Acad@@AEAY01_JVAcDbObjectId@@@Z"

    )]

    private static extern int acdbGetAdsName(

      ref ads_name name, ObjectId objId

    );

 

    public struct ads_name

    {

      IntPtr a;

      IntPtr b;

    };

 

    private Document _doc;

    private FullSubentityPath _path;

    private ObjectId _entId;

    private ObjectId _selId;

    private ObjectId[] _objIds = null;

 

    public ObjectId LastSelected

    {

      get { return _selId; }

    }

 

    public SubEntityHighlighter(Document doc)

    {

      _doc = doc;

      _entId = ObjectId.Null;

      _path = FullSubentityPath.Null;

    }

 

    internal void Unhighlight()

    {

      if (

        _selId.ObjectClass.IsDerivedFrom(

          RXClass.GetClass(typeof(AttributeReference))

        )

      )

      {

        // For attributes we need to use acedRedraw to unhighlight

 

        var ename = new ads_name();

        acdbGetAdsName(ref ename, _selId);

        acedRedraw(ref ename, 4);

        return;

      }

 

      if (!_path.IsNull)

      {

        var tr =

          _doc.TransactionManager.StartOpenCloseTransaction();

        using (tr)

        {

          var ent = (Entity)tr.GetObject(_entId, OpenMode.ForRead);

 

          // ... and highlight the nested entity

 

          ent.Unhighlight(_path, false);

 

          tr.Commit();

 

          _path = FullSubentityPath.Null;

          _entId = ObjectId.Null;

          _selId = ObjectId.Null;

        }

      }

    }

 

    internal void Highlight(

      FullSubentityPath path, ObjectId entId, ObjectId selId

    )

    {

      Unhighlight();

 

      if (

        selId.ObjectClass.IsDerivedFrom(

          RXClass.GetClass(typeof(AttributeReference))

        )

      )

      {

        // For attributes we need to use acedRedraw to highlight

 

        var ename = new ads_name();

        acdbGetAdsName(ref ename, selId);

        acedRedraw(ref ename, 3);

      }

      else

      {

        var tr =

          _doc.TransactionManager.StartOpenCloseTransaction();

        using (tr)

        {

          var ent = (Entity)tr.GetObject(entId, OpenMode.ForRead);

 

          // Highlight the nested entity

 

          ent.Highlight(path, false);

 

          tr.Commit();

        }

      }

 

      _path = path;

      _entId = entId;

      _selId = selId;

    }

 

    internal FullSubentityPath BuildSubEntityPath(

      PromptNestedEntityResult rs,

      out ObjectId outer

    )

    {

      // Get the "containers" list. We store this as member data

      // to save getting it twice. This is the only reason this

      // method is not static...

 

      _objIds = rs.GetContainers();

      var ensel = rs.ObjectId;

      int len = _objIds.Length;

 

      ObjectId[] revIds;

 

      // Reverse the "containers" list

 

      revIds = new ObjectId[len + 1];

      for (int i = 0; i < len; i++)

      {

        var id = (ObjectId)_objIds.GetValue(len - i - 1);

        revIds.SetValue(id, i);

      }

 

      // Now add the selected entity to the end

 

      revIds.SetValue(ensel, len);

 

      outer = revIds[0];

 

      // Retrieve the sub-entity path for this entity

 

      var subEnt =

        new SubentityId(SubentityType.Null, System.IntPtr.Zero);

      return new FullSubentityPath(revIds, subEnt);

    }

 

    internal ObjectId[] GetContainers()

    {

      return _objIds;

    }

  }

 

  public class Commands3

  {

    [CommandMethod("MTIBJIGHL")]

    public void MoveTextInBlock()

    {

   
0; 
var doc = Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Start by getting the text (or other) object in the block

 

      var sehl = new SubEntityHighlighter(doc);

 

      PointMonitorEventHandler handler =

        delegate(object s, PointMonitorEventArgs e)

        {

          // Programmatically select the nested entity under

          // the cursor

 

          var pneo = new PromptNestedEntityOptions("");

          pneo.NonInteractivePickPoint = e.Context.ComputedPoint;

          pneo.UseNonInteractivePickPoint = true;

          var rs = ed.GetNestedEntity(pneo);

          if (rs.Status != PromptStatus.OK)

          {

            // If there wasn't anything there (very common)

            // we'll unhighlight anything highlighted

 

            sehl.Unhighlight();

            return;

          }

 

          // So we have something under the cursor: get the path

          // to it as well as the outermost entity ID

 

          ObjectId outer;

          var path = sehl.BuildSubEntityPath(rs, out outer);

 

          // Highlight the sub-entity

 

          sehl.Highlight(path, outer, rs.ObjectId);

        };

 

      // Set the object snap mode to 0, to be reset afterwards

 

      var os = (short)Application.GetSystemVariable("OSMODE");

      Application.SetSystemVariable("OSMODE", 0);

 

      // Ask for a point to be selected: we'll manually worry about

      // the pseudo-selection process

 

      var ppo = new PromptPointOptions("\nSelect text inside block");

 

      // Add our PointMonitor around the point selection call

 

      ed.PointMonitor += handler;

      var ppr = ed.GetPoint(ppo);

      ed.PointMonitor -= handler;

 

      // Reset the object snap mode right away

 

      Application.SetSystemVariable("OSMODE", os);

 

      // Get the last entity selected (Null if nothing)

 

      var selId = sehl.LastSelected;

 

      // If we don't have a valid ObjectId, don't continue

 

      if (ppr.Status != PromptStatus.OK || selId == ObjectId.Null)

      {

        sehl.Unhighlight();

        return;

      }

 

      // Check the type of object we're dealing with

 

      var oc = selId.ObjectClass;

      if (

        !oc.IsDerivedFrom(

          RXClass.GetClass(typeof(DBText))

        ) &&

        !oc.IsDerivedFrom(

          RXClass.GetClass(typeof(MText))

        )

      )

      {

        // Isn't a text object - ask whether we continue

 

        ed.WriteMessage(

          "\nObject is not text, it is a {0}.", oc.Name

        );

 

        var pko =

          new PromptKeywordOptions(

            "\nDo you want to continue? [Yes/No]", "Yes No"

          );

        pko.AppendKeywordsToMessage = true;

        pko.AllowNone = true;

        pko.Keywords.Default = "No";

 

        var pkr = ed.GetKeywords(pko);

        if (

          pkr.Status != PromptStatus.OK || pkr.StringResult == "No"

        )

        {

          sehl.Unhighlight();

          return;

        }

      }

 

      // Start a transaction to modify the object

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        // Unless we get a block reference container, use the

        // identity matrix as the block transform

 

        var brMat = Matrix3d.Identity;

 

        // Get the containers around the nested entity

 

        var conts = sehl.GetContainers();

        foreach (var brId in conts)

        {

          var br =

            tr.GetObject(brId, OpenMode.ForRead) as BlockReference;

          if (br != null)

          {

            brMat = brMat.PreMultiplyBy(br.BlockTransform);

          }

        }

 

        // Transform the entity

 

        var ent = (Entity)tr.GetObject(selId, OpenMode.ForWrite);

 

        // Before we run the jig, transform the object by the

        // aggregate transform of the containers

 

        ent.TransformBy(brMat);

 

        // Run the jig to displace the object

 

        var dj = new DisplacementJig2(ent, ppr.Value);

        var pr2 = ed.Drag(dj);

 

        sehl.Unhighlight();

 

        if (pr2.Status == PromptStatus.OK)

        {

          // We can transform the entity back, now (at least in

          // terms of the containers: the displacement remains)

 

          ent.TransformBy(brMat.Inverse());

 

          // Open each of the containers and set a property so that

          // they each get regenerated

 

          foreach (var id in conts)

          {

            var ent2 = tr.GetObject(id, OpenMode.ForWrite) as Entity;

            if (ent2 != null)

            {

              // We might also have called this method:

              // ent2.RecordGraphicsModified(true);

              // but setting a property works better with undo

 

              ent2.Visible = ent2.Visible;

            }

          }

          tr.Commit();

        }

      }

    }

  }

}

Here's the command in action, to give you a sense of the results:

MTIBJIGHL

In order to deal with a problem with highlighting attributes, I did end up having to P/Invoke acdbGetAdsName() and acedRedraw(), which I'm not exactly thrilled about. If you don't care about attributes, I strongly suggest ripping that code out. And it only actually works for top-level attributes, anyway: attributes in nested blocks won't get highlighted with it, unfortunately.

If you end up looking into this and finding a nicer way to achieve Samir's request, please do let me know. I started out implementing something using a fairly standard technique – point selection followed by non-interactive nested entity selection – but admittedly the final solution has become messier than I'd have liked. It's very possible someone out there has managed to find a way to get this working perfectly (or better, anyway), in which case I'd very much like to hear from you.

15 responses to “Moving text in an AutoCAD block using .NET – Part 3”

  1. Hi Kean

    I found another problem with the first suggestion (Editor.GetNestedEntity() + PointMonitor) which lies in the use of keywords. If I add keywords to the "outer" PromptNestedEntityOptions (the one that lies outside the PointMonitor), they do appear at the command line but, whenever I select one of them, instead of getting a "PromptStatus.Keyword", I'm getting a "PromptStatus.Cancel", and the StringResult property is also null.

  2. Hi Samir,

    That's not very surprising (but good to know). The dynamic input tooltip issue was just the first indicator of some state being impacted. Best not to go there...

    Regards,

    Kean

  3. Hello Kean.
    I am looking into the possibility of the programmatical creation of blocks and dynamic blocks. So far I haven't been able to figure out in which table the objects that make the blocks dynamic: alignment grip, parameters, actions. I don't know what their object class either.

  4. Hello,

    The last time I checked it wasn't possible to create dynamic blocks programmatically. But you should post to ADN or the AutoCAD .NET Discussion Group, as someone there may know better.

    Non-dynamic blocks are easier, of course. Just populate a new BlockTableRecord with the entities you want to include and add it to the BlockTable.

    Regards,

    Kean

  5. Thanks for the reply and happy new year to you. Now I know that it's not possible but I'm curious as to why, is it because of unexposed Acad features? One more question, is it possible to create useable Acad-object hash numbers in order to compare the objects? Any other way to compare acadDB objects besides property by property? Thanks.

  6. Hello,

    This isn't a support form: please do ask your follow-up questions via ADN or the discussion groups.

    I don't recall the history on the dynamic blocks question, I'm afraid. And comparing objects - determining when objects are equivalent despite certain properties being different (or reversed) is an application category in itself (there are a number of "drawing compare" apps in the Exchange Store, for instance).

    Regards,

    Kean

  7. Hi Kean,

    Can this code be applicable in the case of 'overrule'?
    Is there a way to select/move primitives drawn under overrule.

    Thanks.
    Igor

  8. Hi Igor,

    You could presumably capture the overruled graphics being generated and do some analysis to determine whether the object is being hovered over... it might get complicated but should be possible.

    Regards,

    Kean

  9. Alexandr Shchetinin Avatar
    Alexandr Shchetinin

    Hi Kean

    acedRedraw(NULL,1) in C++ is identical to the AutoCAD REDRAW command.
    Can I call acedRedraw from C# with NULL first argument?

    1. Kean Walmsley Avatar

      Hi Alexandr,

      I would think so. I don't recall having done so, myself, but if it works...

      Cheers,

      Kean

  10. Jürgen A. Becker Avatar
    Jürgen A. Becker

    Hi Kean,
    can you tell me the EntryPoint "?acdbGetAdsName@@YA?AW4ErrorStatus@Acad@@AEAY01_JVAcDbObjectId@@@Z" for AutoCAD 2017 and 2018.
    Thanks
    Jürgen

    1. Sorry, Jurgen: I only have 2018 installed, and I really don't have time to help with this right now. Please post to the discussion groups and ask someone there to help.

      Kean

      1. Jürgen A. Becker Avatar

        Hi Kean,
        no Problem. I'Ve got the solution.
        It's the same EntryPoint. Jst change the DLL Name according to your AutoCAD version. Thats all.
        Thanks KEan.
        Cheers Jürgen

  11. Sergey Suloev Avatar

    hi Kean,

    I am looking for a way to select MText nested inside a block reference.
    My problem is that ACAD 2020 does not allow to select MText at all. Even if the user clicks (double clicks) directly above the Mtext the only entity that can be selected is the parent block.
    I would like to select the block by a single click and, if the cursor is above the MText , make a double-click to select the MText.
    Do you have any example on this (or similar) problem with C/C++ ObjectARX API ? Or maybe you can give a hint on a possible a way I should go.
    Thank you so much for sharing you knowledge.

    1. Kean Walmsley Avatar

      Hi Sergey,

      Sorry for the delayed reply - I just found the notification in my spam folder.

      I'm no longer working regularly with AutoCAD. Please post your technical problems via the AutoCAD .NET (or ObjectARX) forum.

      Many thanks,

      Kean

Leave a Reply to DouceDeux Cancel reply

Your email address will not be published. Required fields are marked *