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:
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:
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

Atom

