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.

July 13, 2007 in AutoCAD, AutoCAD .NET, Fields | Permalink | Comments (14) | TrackBack

Creating a table of block attributes in AutoCAD using .NET - Part 2

In the last post we looked at some code to create a table of attribute values for a particular block. In this post we'll extend that code and show how to use a formula to create a total of those values.

Below is the C# code. I've numbered the lines, and those in red are new since the last post. The complete source file can be downloaded here.

Firstly, a quick breakdown of the changes:

  • Lines 60-81 deal with user input, and the forcing of the decision to "embed" rather than "link", if we're performing the total (table formulae do not work with fields, even if they have numeric results, so we're forced to create the table with the current value, rather than a field pointing to the attribute reference)
  • Line 134 and subsequently lines 159-166 declare and set a variable indicating for which column we're going to provide a total
  • Lines 169-181 deal with the exceptional case that we don't find the specified attribute definition
  • Lines 310-336 create our additional row, and insert the total in the appropriate cell. We're using a formula such as this: %<\AcExpr (Sum(A2:A4)) \f "%lu2%pr2">%
    • The \f flag specifies we want a numeric value with 2 decimal places - these codes are not documented, but you can find them out by using the FIELD command, as described in this previous post
  • Line 343 performs a regen, to update the value of our field

And now for the code:

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.DatabaseServices;

    3 using Autodesk.AutoCAD.EditorInput;

    4 using Autodesk.AutoCAD.Geometry;

    5 using Autodesk.AutoCAD.Runtime;

    6 using System.Collections.Specialized;

    7 using System;

    8

    9 namespace TableCreation

   10 {

   11   public class Commands

   12   {

   13     // Set up some formatting constants

   14     // for the table

   15

   16     const double colWidth = 15.0;

   17     const double rowHeight = 3.0;

   18     const double textHeight = 1.0;

   19     const CellAlignment cellAlign =

   20       CellAlignment.MiddleCenter;

   21

   22     // Helper function to set text height

   23     // and alignment of specific cells,

   24     // as well as inserting the text

   25

   26     static public void SetCellText(

   27       Table tb,

   28       int row,

   29       int col,

   30       string value

   31     )

   32     {

   33       tb.SetAlignment(row, col, cellAlign);

   34       tb.SetTextHeight(row, col, textHeight);

   35       tb.SetTextString(row, col, value);

   36     }

   37

   38     [CommandMethod("BAT")]

   39     static public void BlockAttributeTable()

   40     {

   41       Document doc =

   42         Application.DocumentManager.MdiActiveDocument;

   43       Database db = doc.Database;

   44       Editor ed = doc.Editor;

   45

   46       // Ask for the name of the block to find

   47

   48       PromptStringOptions opt =

   49         new PromptStringOptions(

   50           "\nEnter name of block to list: "

   51         );

   52       PromptResult pr = ed.GetString(opt);

   53

   54       if (pr.Status == PromptStatus.OK)

   55       {

   56         string blockToFind =

   57           pr.StringResult.ToUpper();

   58         bool embed = false;

   59

   60         // And the attribute to provide total for

   61

   62         opt.Message =

   63           "\nEnter name of column to total <\"\">: ";

   64         pr = ed.GetString(opt);

   65

   66         if (pr.Status == PromptStatus.None ||

   67             pr.Status == PromptStatus.OK)

   68         {

   69           string columnToTotal =

   70               pr.StringResult.ToUpper();

   71

   72           if (columnToTotal != "")

   73           {

   74             // If a column has been chosen, we need

   75             // to embed the attribute values

   76             // as otherwise the "sum" formula will fail

   77

   78             embed = true;

   79           }

   80           else

   81           {

   82             // Ask whether to embed or link

   83

   84             PromptKeywordOptions pko =

   85               new PromptKeywordOptions(

   86                 "\nEmbed or link the attribute values: "

   87               );

   88

   89             pko.AllowNone = true;

   90             pko.Keywords.Add("Embed");

   91             pko.Keywords.Add("Link");

   92             pko.Keywords.Default = "Embed";

   93             PromptResult pkr =

   94               ed.GetKeywords(pko);

   95

   96             if (pkr.Status == PromptStatus.None ||

   97                 pkr.Status == PromptStatus.OK)

   98             {

   99               if (pkr.Status == PromptStatus.None ||

  100                   pkr.StringResult == "Embed")

  101                 embed = true;

  102               else

  103                 embed = false;

  104             }

  105           }

  106

  107           Transaction tr =

  108             doc.TransactionManager.StartTransaction();

  109           using (tr)

  110           {

  111             // Let's check the block exists

  112

  113             BlockTable bt =

  114               (BlockTable)tr.GetObject(

  115                 doc.Database.BlockTableId,

  116                 OpenMode.ForRead

  117               );

  118

  119             if (!bt.Has(blockToFind))

  120             {

  121               ed.WriteMessage(

  122                 "\nBlock "

  123                 + blockToFind

  124                 + " does not exist."

  125               );             

  126             }

  127             else

  128             {

  129               // And go through looking for

  130               // attribute definitions

  131

  132               StringCollection colNames =

  133                 new StringCollection();

  134               int colToTotalIdx = -1;

  135

  136               BlockTableRecord bd =

  137                 (BlockTableRecord)tr.GetObject(

  138                   bt[blockToFind],

  139                   OpenMode.ForRead

  140                 );

  141               foreach (ObjectId adId in bd)

  142               {

  143                 DBObject adObj =

  144                   tr.GetObject(

  145                     adId,

  146                     OpenMode.ForRead

  147                   );

  148

  149                 // For each attribute definition we find...

  150

  151                 AttributeDefinition ad =

  152                   adObj as AttributeDefinition;

  153                 if (ad != null)

  154                 {

  155                   // ... we add its name to the list

  156

  157                   colNames.Add(ad.Tag);

  158

  159                   if (ad.Tag.ToUpper() == columnToTotal)

  160                   {

  161                     // Save the index of the column

  162                     // we want to total

  163

  164                     colToTotalIdx =

  165                       colNames.Count - 1;

  166                   }

  167                 }

  168               }

  169               // If we didn't find the attribute to be totalled

  170               // then simply ignore the request and continue

  171

  172               if (columnToTotal != "" && colToTotalIdx < 0)

  173               {

  174                 ed.WriteMessage(

  175                   "\nAttribute definition for "

  176                   + columnToTotal

  177                   + " not found in "

  178                   + blockToFind

  179                   + ". Total will not be added to the table."

  180                 );

  181               }

  182               if (colNames.Count == 0)

  183               {

  184                 ed.WriteMessage(

  185                   "\nThe block "

  186                   + blockToFind

  187                   + " contains no attribute definitions."                  

  188                 );

  189               }

  190               else

  191               {

  192                 // Ask the user for the insertion point

  193                 // and then create the table

  194

  195                 PromptPointResult ppr =

  196                   ed.GetPoint(

  197                     "\nEnter table insertion point: "

  198                   );

  199

  200                 if (ppr.Status == PromptStatus.OK)

  201                 {

  202                   Table tb = new Table();

  203                   tb.TableStyle = db.Tablestyle;

  204                   tb.NumRows = 1;

  205                   tb.NumColumns = colNames.Count;

  206                   tb.SetRowHeight(rowHeight);

  207                   tb.SetColumnWidth(colWidth);

  208                   tb.Position = ppr.Value;

  209

  210                   // Let's add our column headings

  211

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

  213                   {

  214                     SetCellText(tb, 0, i, colNames[i]);

  215                   }

  216

  217                   // Now let's search for instances of

  218                   // our block in the modelspace

  219

  220                   BlockTableRecord ms =

  221                     (BlockTableRecord)tr.GetObject(

  222                       bt[BlockTableRecord.ModelSpace],

  223                       OpenMode.ForRead

  224                     );

  225

  226                   int rowNum = 1;

  227                   foreach (ObjectId objId in ms)

  228                   {

  229                     DBObject obj =

  230                       tr.GetObject(

  231                         objId,

  232                         OpenMode.ForRead

  233                       );

  234                     BlockReference br =

  235                       obj as BlockReference;

  236                     if (br != null)

  237                     {

  238                       BlockTableRecord btr =

  239                         (BlockTableRecord)tr.GetObject(

  240                           br.BlockTableRecord,

  241                           OpenMode.ForRead

  242                         );

  243                       using (btr)

  244                       {

  245                         if (btr.Name.ToUpper() == blockToFind)

  246                         {

  247                           // We have found one of our blocks,

  248                           // so add a row for it in the table

  249

  250                           tb.InsertRows(

  251                               rowNum,

  252                               rowHeight,

  253                               1

  254                           );

  255

  256                           // Assume that the attribute refs

  257                           // follow the same order as the

  258                           // attribute defs in the block

  259

  260                           int attNum = 0;

  261                           foreach (

  262                             ObjectId arId in

  263                             br.AttributeCollection

  264                           )

  265                           {

  266                             DBObject arObj =

  267                               tr.GetObject(

  268                                 arId,

  269                                 OpenMode.ForRead

  270                               );

  271                             AttributeReference ar =

  272                               arObj as AttributeReference;

  273                             if (ar != null)

  274                             {

  275                               // Embed or link the values

  276

  277                               string strCell;

  278                               if (embed)

  279                               {

  280                                 strCell = ar.TextString;

  281                               }

  282                               else

  283                               {

  284                                 string strArId =

  285                                   arId.ToString();

  286                                 strArId =

  287                                   strArId.Trim(

  288                                     new char[] { '(', ')' }

  289                                   );

  290                                 strCell =

  291                                   "%<\\AcObjProp Object("

  292                                     + "%<\\_ObjId "

  293                                     + strArId

  294                                     + ">%).TextString>%";

  295                               }

  296                               SetCellText(

  297                                 tb,

  298                                 rowNum,

  299                                 attNum,

  300                                 strCell

  301                               );

  302                             }

  303                             attNum++;

  304                           }

  305                           rowNum++;

  306                         }

  307                       }

  308                     }

  309                   }

  310

  311                   // Now let's add a row for our total

  312

  313                   if (colToTotalIdx >= 0)

  314                   {

  315                     tb.InsertRows(rowNum, rowHeight, 1);

  316                     char colLetter =

  317                       Convert.ToChar(

  318                         (Convert.ToInt32(

  319                           'A') + colToTotalIdx

  320                         )

  321                       );

  322

  323                     // Add a formula to sum the column

  324

  325                     SetCellText(

  326                       tb,

  327                       rowNum,

  328                       colToTotalIdx,

  329                       "%<\\AcExpr (Sum("

  330                         + colLetter

  331                         + "2:"

  332                         + colLetter

  333                         + rowNum.ToString()

  334                         + ")) \\f  \"%lu2%pr2\">%"

  335                     );

  336                   }

  337                   tb.GenerateLayout();

  338

  339                   ms.UpgradeOpen();

  340                   ms.AppendEntity(tb);

  341                   tr.AddNewlyCreatedDBObject(tb, true);

  342                   tr.Commit();

  343                   ed.Regen();

  344                 }

  345               }

  346             }

  347           }

  348         }

  349       }

  350     }

  351   }

  352 }

Here's what happens when you run the updated BAT command against the data I used last time:

Attribute_table_2

June 18, 2007 in AutoCAD, AutoCAD .NET, Blocks, Fields, Tables | Permalink | Comments (1) | TrackBack

Creating a table of block attributes in AutoCAD using .NET - Part 1

This post was inspired by suggestions from a few different people (you know who you are! :-). I'm going to take it in two parts: this post will focus on creating a table automatically that lists the values of attribute references included in block references in the modelspace that point to a particular block table record selected by the user. Phew. The next post will add some functionality to create a "total" of one of the columns in the table we create, by using a table formula that performs a sum of the appropriate cells.

The below code is actually quite similar in behaviour to the Table sample on the ObjectARX SDK and also the EATTEXT command inside AutoCAD - both of which will help you create tables from block attributes. I wrote this code in AutoCAD 2007 (and it should work just fine in 2008, also). I haven't tested against prior versions.

One item of note is the ability to either embed or link the data placed in the table. "Embedding" means we just take a copy of the attribute values and place them as plain text in the cells; "linking" means we use a field to create a reference from the cell to the attribute's value (using the technique shown in the previous post).

The code is quite lengthy, but I've done my best to comment it to make it more clear what's going on. Here's the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using System.Collections.Specialized;

using System;


namespace TableCreation

{

  public class Commands

  {

    // Set up some formatting constants

    // for the ta