Kean Walmsley

July 2009

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Twitter Updates

    follow me on Twitter



    « Free advanced AutoCAD .NET/ObjectARX webcasts | Main | Using a palette from .NET to display properties of multiple AutoCAD objects »

    July 13, 2007

    Accessing the AutoCAD objects referred to by fields using .NET

    Thanks to Wolfgang Ruthensteiner for suggesting this excellent topic a comment to this previous post. Here's Wonfgang's question:

    How do I read back the field code with C# (from an attribute e.g.)?

    I am linking room-label blocks with polylines, using fields inside an attribute to display the polyline's area property.

    Later I want to find out programatically, which polyline a certain block is linked to by evaluating the field in the attribute (extracting the objectId).

    This was actually quite tricky, and one I needed the help of our old friend, ArxDbg, to solve (see here for some information on this very useful ObjectARX sample). I should say up-front that there may well be a simpler way to access the information - the below technique is to some degree relying on the database structure (which might be considered an implementation detail). I may be missing a higher-level API providing a simpler way to access the information, but there you have it.

    The full text of the field expression is stored in an AcDbField object (which is accesible through the Autodesk.AutoCAD.DatabaseServices.Field) which exists inside a field dictionary in the text object's (or attribute's) extension dictionary. So here's what needs to happen:

    • Select the MText object (I chose to use MText in the below code, as it was a bit more work to allow attribute selection within a block - left as an exercise for the reader :-)
    • Open the MText object's extension dictionary
    • Open the nested field dictionary
    • Access the field object stored therein

    At this stage you have your text string with all the uninterpreted field codes. For those of you that are interested, I remember an important decision at the time we implemented fields in AutoCAD: that we should maintain the existing protocol and not return uninterpreted field codes from the standard text access properties/methods. This was largely to avoid migration issues for applications that depended on the data to be returned in its evaluated form. But it clearly means a bit more work if you want to get at the underlying codes.

    So once we have our codes, we then want to get back to the "referred" object(s). I implemented a simple function that parses a string for the following sub-string:

    %<\_ObjId XXX>%

    ... where XXX is a string representing the ObjectId. The code then uses a conversion function to get an integer from the string, and create an ObjectId from the integer. We return the ID to the calling function, where we can then open it and find out more about it.

    So that's the description - here's the C# code implementing it:

    using Autodesk.AutoCAD.ApplicationServices;

    using Autodesk.AutoCAD.DatabaseServices;

    using Autodesk.AutoCAD.EditorInput;

    using Autodesk.AutoCAD.Runtime;

    using System;


    namespace FieldExtraction

    {

      public class Commands

      {

        [CommandMethod("GFL")]

        static public void GetFieldLink()

        {

          Document doc =

            Application.DocumentManager.MdiActiveDocument;

          Database db = doc.Database;

          Editor ed = doc.Editor;


          // Ask the user to select an attribute or an mtext

          PromptEntityOptions opt =

            new PromptEntityOptions(

              "\nSelect an MText object containing field(s): "

            );

          opt.SetRejectMessage(

            "\nObject must be MText."

          );

          opt.AddAllowedClass(typeof(MText), false);

          PromptEntityResult res =

            ed.GetEntity(opt);


          if (res.Status == PromptStatus.OK)

          {

            Transaction tr =

              doc.TransactionManager.StartTransaction();

            using (tr)

            {

              // Check the entity is an MText object

              DBObject obj =

                tr.GetObject(

                  res.ObjectId,

                  OpenMode.ForRead

                );


              MText mt = obj as MText;

              if (mt != null)

              {

                if (!mt.HasFields)

                {

                  ed.WriteMessage(

                    "\nMText object does not contain fields."

                  );

                }

                else

                {

                  // Open the extension dictionary

                  DBDictionary extDict =

                    (DBDictionary)tr.GetObject(

                      mt.ExtensionDictionary,

                      OpenMode.ForRead

                    );


                  const string fldDictName = "ACAD_FIELD";

                  const string fldEntryName = "TEXT";

                  // Get the field dictionary

                  if (extDict.Contains(fldDictName))

                  {

                    ObjectId fldDictId =

                      extDict.GetAt(fldDictName);

                    if (fldDictId != ObjectId.Null)

                    {

                      DBDictionary fldDict =

                        (DBDictionary)tr.GetObject(

                          fldDictId,

                          OpenMode.ForRead

                        );


                      // Get the field itself

                      if (fldDict.Contains(fldEntryName))

                      {

                        ObjectId fldId =

                          fldDict.GetAt(fldEntryName);

                        if (fldId != ObjectId.Null)

                        {

                          obj =

                            tr.GetObject(

                              fldId,

                              OpenMode.ForRead

                            );

                          Field fld = obj as Field;

                          if (fld != null)

                          {

                            // And finally get the string

                            // including the field codes

                            string fldCode = fld.GetFieldCode();

                            ed.WriteMessage(

                              "\nField code: "

                              + fldCode

                            );


                            // Loop, using our helper function

                            // to find the object references

                            do

                            {

                              ObjectId objId;

                              fldCode =

                                FindObjectId(

                                  fldCode,

                                  out objId

                                );

                              if (fldCode != "")

                              {

                                // Print the ObjectId

                                ed.WriteMessage(

                                  "\nFound Object ID: "

                                  + objId.ToString()

                                );

                                obj =

                                  tr.GetObject(

                                    objId,

                                    OpenMode.ForRead

                                  );

                                // ... and the type of the object

                                ed.WriteMessage(

                                  ", which is an object of type "

                                  + obj.GetType().ToString()

                                );

                              }

                            } while (fldCode != "");                         

                          }

                        }

                      }

                    }

                  }

                }

              }

            }

          }

        }


        // Extract an ObjectId from a field string

        // and return the remainder of the string

        //

        static public string FindObjectId(

          string text,

          out ObjectId objId

        )

        {

          const string prefix = "%<\\_ObjId ";

          const string suffix = ">%";


          // Find the location of the prefix string

          int preLoc = text.IndexOf(prefix);

          if (preLoc > 0)

          {

            // Find the location of the ID itself

            int idLoc = preLoc + prefix.Length;


            // Get the remaining string

            string remains = text.Substring(idLoc);


            // Find the location of the suffix

            int sufLoc = remains.IndexOf(suffix);


            // Extract the ID string and get the ObjectId

            string id = remains.Remove(sufLoc);

            objId = new ObjectId(Convert.ToInt32(id));


            // Return the remainder, to allow extraction

            // of any remaining IDs

            return remains.Substring(sufLoc + suffix.Length);

          }

          else

          {

            objId = ObjectId.Null;

            return "";

          }

        }

      }

    }

    Here's what happens when we run the code. Firstly I went and created a simple, closed polyline and a circle. I then created a single MText object with field codes accessing the other two objects' areas:

    Fields_4

    I then run the GFL command and select the MText object:

    Command: GFL

    Select an MText object containing field(s):

    Field code: Area of the circle: \AcObjProp Object(%<\_ObjId

    2130239616>%).Area\P\PArea of the polyline: \AcObjProp Object(%<\_ObjId

    2130239624>%).Area

    Found Object ID: (2130239616), which is an object of type

    Autodesk.AutoCAD.DatabaseServices.Circle

    Found Object ID: (2130239624), which is an object of type

    Autodesk.AutoCAD.DatabaseServices.Polyline

    As you can see, we've been able to find and extract information from the objects referred to by fields in an MText object.

    TrackBack

    TrackBack URL for this entry:
    http://www.typepad.com/services/trackback/6a00d83452464869e200e0099252af8833

    Listed below are links to weblogs that reference Accessing the AutoCAD objects referred to by fields using .NET:

    Comments

    Hi Kean!

    Thank you very much for your answer - that was fast!!!!!

    I'm working with AutoCAD 2006 and there is no Field Object available in the Autodesk.AutoCAD.DatabaseServices Namespace. I figured out quite a lot today and already considered writing a managed wrapper for the AcDbField Object. Here is what i came up with so far:

    ///
    /// a starting point for an AcDbFieldWrapper
    /// i guess it has to be implemented with C++ in a separate wrapper dll
    /// HOW?
    ///
    class AcDbFieldWrapper
    {
    IntPtr intPtr;
    ObjectId fieldId;

    public AcDbFieldWrapper(ObjectId fieldId)
    {
    this.fieldId = fieldId;
    TransactionManager tm = fieldId.Database.TransactionManager;
    using (DBObject field = (DBObject)tm.GetObject(fieldId, OpenMode.ForRead, true,true))
    {
    this.dBObjectType = field.GetType(); //Autodesk.AutoCAD.DatabaseServices.ImpDBObject ???
    this.intPtr = field.UnmanagedObject;
    }
    }

    private Type dBObjectType;

    public Type DBObjectType
    {
    get { return dBObjectType; }
    }


    public string GetFieldCode()
    {
    throw new System.Exception("AcDbFieldWrapper.GetFieldCode() not implemented");
    }

    ///
    /// static method to extract a fieldId from an attributeId
    /// if the attribute contains a field
    ///
    /// ObjectId of an attribute reference
    /// ObjectId of a DBObject of type Autodesk.AutoCAD.DatabaseServices.ImpDBObject that probably is an AcDbField
    public static ObjectId GetFieldFromAttribute(ObjectId attId)
    {
    const string ACAD_FIELD = "ACAD_FIELD";
    const string TEXT = "TEXT";
    TransactionManager tm = attId.Database.TransactionManager;
    using (AttributeReference attr = tm.GetObject(attId, OpenMode.ForRead, true, true) as AttributeReference)
    {
    if ((attr != null) && attr.ExtensionDictionary.IsValid)
    {
    using (DBDictionary dic = (DBDictionary)tm.GetObject(attr.ExtensionDictionary, OpenMode.ForRead, true, true))
    {
    if (dic.Contains(ACAD_FIELD))
    {
    using (DBDictionary fdic = (DBDictionary)tm.GetObject(dic.GetAt(ACAD_FIELD), OpenMode.ForRead, true, true))
    {
    if (fdic.Contains(TEXT))
    {
    return fdic.GetAt(TEXT); //this probably returns an AcDbField's ObjectId
    }
    }
    }
    }
    }
    }
    return ObjectId.Null;
    }
    }

    public void TestAttributes(string fileName)
    {
    using (Database db = new Database(false, true))
    {

    db.ReadDwgFile(fileName, System.IO.FileShare.ReadWrite, true, null);
    TransactionManager tm = db.TransactionManager;
    using (Transaction t = tm.StartTransaction())
    {
    BlockTable bt = (BlockTable)tm.GetObject(db.BlockTableId, OpenMode.ForRead, false);
    BlockTableRecord modelSpace = (BlockTableRecord)tm.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead, false);

    foreach (ObjectId id in modelSpace)
    {
    BlockReference br = tm.GetObject(id, OpenMode.ForRead) as BlockReference;
    if (br != null)
    {
    foreach (ObjectId attID in br.AttributeCollection)
    {
    ObjectId fieldId = AcDbFieldWrapper.GetFieldFromAttribute(attID);
    if (fieldId != ObjectId.Null)
    {
    AcDbFieldWrapper acDbField = new AcDbFieldWrapper(fieldId);
    Debug.WriteLine("Attribute contains a field, Type: " + acDbField.DBObjectType.ToString());
    }
    }
    }
    }
    }

    }
    }


    Do I have to upgrade my AutoCAD or is there another way?

    W

    Hi Wolfgang,

    I'm not an expert at exposing managed wrappers, I'm afraid. Members of my team do it regularly, so asking via ADN would get you an answer. If you do happen to be an ADN member then there's also a fair amount of info on the ADN website that should be of interest, for example:

    Tutorial to create managed wrappers for custom ARX functions and objects

    If you're not an ADN member then let me know and I'll send you this particular document by email.

    That said, upgrading could prove to be more cost effective if you don't have many seats and you're not proficient with managed C++ (which I will freely admit I'm not).

    Regards,

    Kean

    Hi Kean!

    Thank you very much for all your help! I want to give it a try and create my own wrapper. I've done it before, but not for an AutoCAD project and I'm not a C++ expert either. I'm not an ADN Member, so I would highly appreciate if you could send me the document. I just found out that my e-mail adress posted with my last messages was wrong. Now it's corrected...

    Thank you again!

    W.

    Hi Kean,
    thank you for this code. I had really big problems to get the field code of attributes. Now it should be no problem anymore.

    Roland

    Hi Kean,
    with you're code I can get the field code of attributes, but can you explain how to write some field code into attributes or texts. It seems to work a little bit different with tables as for texts.

    I would need it for a program which inserts blocks and its attributes. The attributes includes a field eg. with the position of the block. Now when I insert the attributes the fieldcode is not correct because it does not know the Id of the block. Therefore I have to read the field code and change it.

    Regards
    Roland

    Hi Roland,

    I'm not sure why this works differently - can you email me some code that demonstrates the problem you're hitting?

    Regards,

    Kean

    Hi Kean,
    here is the code I'm testing.
    But the attribute only shows the code of the field.

    using Autodesk.AutoCAD.ApplicationServices;
    using Autodesk.AutoCAD.DatabaseServices;
    using Autodesk.AutoCAD.EditorInput;
    using Autodesk.AutoCAD.Runtime;
    using Autodesk.AutoCAD.Geometry;
    using Autodesk.AutoCAD.Internal;

    [assembly: CommandClass(typeof(RSNNAcadApp.Test.InsertBlock))]
    namespace RSNNAcadApp.Test
    {
    public class InsertBlock
    {
    //Inserts a blockreference at Point 0,0,0
    [CommandMethod("InsertTest")]
    static public void InsertBlockTest()
    {
    ObjectId tmpBlockId;

    Document doc = Application.DocumentManager.MdiActiveDocument;
    Database db = doc.Database;
    Editor ed = doc.Editor;
    Transaction tr = doc.TransactionManager.StartTransaction();
    try
    {
    using (tr)
    {
    //Get Blockname and Id
    PromptStringOptions BlockNameOption = new PromptStringOptions("Blockname:");
    BlockNameOption.AllowSpaces = false;
    PromptResult BlockNameResult = ed.GetString(BlockNameOption);
    BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead, false);

    if (BlockNameResult.Status == PromptStatus.OK)
    {
    string tmpBlockName = BlockNameResult.StringResult;

    tmpBlockId = bt[tmpBlockName];

    BlockTableRecord btr = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);

    Point3d ObjPunkt = Point3d.Origin;
    BlockReference BlockRef = new BlockReference(ObjPunkt, tmpBlockId);
    ObjectId BlObj = btr.AppendEntity(BlockRef);
    tr.AddNewlyCreatedDBObject(BlockRef, true);

    BlockTableRecord btAttRec = (BlockTableRecord)tr.GetObject(tmpBlockId, OpenMode.ForRead);

    if (btAttRec.Annotative)
    {
    //Attach Current Annotation-Scale.
    //If you don't add the content the block and the following attribute will not inserted correct.
    ObjectContextManager ocm = db.ObjectContextManager;
    ObjectContextCollection occ = ocm.GetContextCollection("ACDB_ANNOTATIONSCALES");

    DBObject obj = tr.GetObject(BlObj, OpenMode.ForRead);
    if (obj != null)
    {
    //ObjectContexts.AddContext(obj, occ.GetContext("1:1"));
    ObjectContexts.AddContext(obj, occ.CurrentContext);
    }
    }

    ed.WriteMessage(string.Format("\nBlockID: {0}", BlObj.ToString()));
    //Add the attributes
    foreach (ObjectId idAtt in btAttRec)
    {
    Entity ent = (Entity)tr.GetObject(idAtt, OpenMode.ForRead);
    if (ent is AttributeDefinition)
    {
    AttributeDefinition attDef = (AttributeDefinition)ent;
    AttributeReference attRef = new AttributeReference();
    attRef.SetAttributeFromBlock(attDef, BlockRef.BlockTransform);
    ObjectId AttObj = BlockRef.AttributeCollection.AppendAttribute(attRef);

    string FieldText = "\\AcVar Login \\f \"%tc1\"";
    attRef.TextString = FieldText;

    tr.AddNewlyCreatedDBObject(attRef, true);
    }
    }
    }


    tr.Commit();
    }
    }
    catch (System.Exception ex)
    {
    ed.WriteMessage(ex.ToString());
    }
    finally
    {
    tr.Dispose();
    }

    }

    }
    }

    Regards,
    Roland

    Hi Roland,

    You're missing the field delimiter - try this instead:

    string FieldText = "%<\\AcVar Login \\f \"%tc1\">%";

    The field values will probably come up as "####" until they're regened, so adding this after tr.Commit() will fix that:

    ed.Regen();

    Regards,

    Kean

    Sometimes it is so simple. It takes me hours of testing and it was just such a little thing.

    Thak you, Kean.

    Now just another question.
    If there is a field code like the following in the attributedeffinition, is it possible to insert the attributereference with the correct field code.

    The code from the attribute deffinition is:
    \AcObjProp.16.2 Object(?BlockRefId,1).InsertionPoint \f
    "%lu2%pt2%pr3"

    after inserting the attribute reference the field code of it is:
    %<\AcObjProp \f "%lu2%pt2%pr3">%

    I understand that it doesn't know the ObjectId of the BlockReference. But is there a simple way to insert the correct code?
    Or do I have to read the field code of the attribute deffinition and need to change "\AcObjProp.16.2 Object(?BlockRefId,1)" to e.g. "AcObjProp Object(%<\_ObjId 2130518272>%)" manually?

    Regards
    Roland

    Hi Roland,

    I haven't seen the syntax you've used here (Object(?BlockRefId,1)) - I'm only familiar with the _ObjId field code. Is this pseudo-code, that you'd like to have work, or is it from an actual object? I wasn't aware (or don't believe) it's possible without going via the ObjectId (although the process to set it can be automated - it doesn't need to be manual).

    Regards,

    Kean

    Thank you, Kean.
    The code above is from a block placeholder (I hope it is the right name in english) for the insertion point of the block.
    After inserting the block with the command INSERT the field code changes and the correct ObjId is filled in and the y-Value will be seen. If I do it with the above code the result is something without any ObjectId and you can just see "InsertionPoint" as the field value. But now it is no problem anymore to search and replace the code from the attdef and fill in the right code into the attref, I just thought that there is something which is doing it automatically.

    Regards,
    Roland

    Hi Kean,
    just another question. Is it possible to get the field code with the field delimiter?
    When you look at your example it is not really easy to see where the field code begins and where it ends. Therefore it is difficult to change something in the text without loosing some field code.

    Regards,
    Roland

    Hi Roland,

    I chose to make FindObjectId() as flexible as possible (for my needs), by defining a prefix and a suffix to search for. You should feel free to reimplement this to be more suitable to your own needs... :-)

    Regards,

    Kean

    Verify your Comment

    Previewing your Comment

    This is only a preview. Your comment has not yet been posted.

    Working...
    Your comment could not be posted. Error type:
    Your comment has been posted. Post another comment

    The letters and numbers you entered did not match the image. Please try again.

    As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

    Having trouble reading this image? View an alternate.

    Working...

    Post a comment

    Feed & Share

    Search