Working with specific AutoCAD object types in .NET
Most of the functions to access objects in the AutoCAD drawing return generic objects/entities. The base type for objects stored in the AutoCAD database is “DBObject” – which can be used to access common persistence information such as the handle, etc. – but there is no such thing as a pure DBObject: all DBObjects actually belong to a type that is derived from this base (whether that’s a “Line”, “Circle”, “BlockReference”, etc.).
The next level up in the hierarchy is “Entity”, from where you can access graphical properties such as color, layer, linetype, material, etc. The sample code in this previous entry shows how to treat a DBObject as an Entity – in this entry we’re going to look at how to access information from an even more specific type, such as the center point and radius of a Circle.
In order to find out what kind of object you’re dealing with, you need to use some kind of runtime type system. A compile-time type system is not going to be enough, as in many cases you simply don’t know what type of object it is you’re going to find in a particular location in the drawing database – especially when asking the user to select entities or when reading them from one of the block table records such as the model-space.
C++ introduced a system for RTTI (RunTime Type Identification) after ObjectARX was first implemented, so AutoCAD maintains its own class hierarchy in memory. In order to use a more specific class in ObjectARX than the generic AcDbObject, you typically use isKindOf() or cast(), which ultimately use the AcDb class hierarchy behind the scenes to determine whether the pointer conversion operation is safe. The C++ standard now includes dynamic_cast<> to perform the equivalent task, but this is not enabled for standard ObjectARX types. As far as I recall, it is enabled for some of our other APIs (such as the Object Modeling Framework, the C++ API in Architectural Desktop that sits on top of ObjectARX), but for ObjectARX the existing type system has proven adequate until now.
Here’s some ObjectARX code showing this:
// objId is the object ID of the entity we want to access
Acad::ErrorStatus es;
AcDbEntity *pEnt = NULL;
es = acdbOpenAcDbEntity( pEnt, objId, AcDb::kForRead );
if ( es == Acad::eOk )
{
AcDbCircle *pCircle = NULL;
pCircle = AcDbCircle::cast( pEnt );
if ( pCircle )
{
// Access circle-specific properties/methods here
AcGePoint3d cen = pCircle->center();
// ...
}
pEnt->close();
}
In a managed environment you get access to the .NET type system. Here’s an example of what you might do in VB.NET:
[Note: the following two fragments have been pointed out in comments as being sub-optimal - please see further down for a better technique...]
' tr is the running transaction
' objId is the object ID of the entity we want to access
Dim obj As DBObject = tr.GetObject(objId, OpenMode.ForRead)
Try
Dim circ As Circle = CType(obj, Circle)
' Access circle-specific properties/methods here
' ...
Catch ex As InvalidCastException
' That's fine - it's just not a circle...
End Try
obj.Dispose()
And in C#:
// tr is the running transaction
// objId is the object ID of the entity we want to access
DBObject obj = tr.GetObject(objId, OpenMode.ForRead);
try
{
Circle circ = (circle)obj;
// Access circle-specific properties/methods here
// ...
}
catch (InvalidCastException ex)
{
// That's fine - it's just not a circle...
}
obj.Dispose();
[Here is the more elegant way to code this...]
VB.NET:
' tr is the running transaction
' objId is the object ID of the entity we want to access
Dim obj As DBObject = tr.GetObject(objId, OpenMode.ForRead)
If TypeOf (obj) Is Circle Then
Dim circ As Circle = CType(obj, Circle)
' Access circle-specific properties/methods here
' ...
End If
obj.Dispose()
C#:
// tr is the running transaction
// objId is the object ID of the entity we want to access
DBObject obj = tr.GetObject(objId, OpenMode.ForRead);
Circle circ = obj as Circle;
if (circ != null)
{
// Access circle-specific properties/methods here
// ...
}
obj.Dispose();
So now let's plug that technique into the previous sample. All we're going to do is check whether each entity that was selected is a Circle, and if so, print out its radius and center point in addition to the common entity-level properties we listed in last time.
Here's the VB.NET version:
Imports Autodesk.AutoCAD
Imports Autodesk.AutoCAD.Runtime
Imports Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.EditorInput
Namespace SelectionTest
Public Class PickfirstTestCmds
' Must have UsePickSet specified
<CommandMethod("PFT", _
(CommandFlags.UsePickSet _
Or CommandFlags.Redraw _
Or CommandFlags.Modal))> _
Public Shared Sub PickFirstTest()
Dim doc As Document = _
Application.DocumentManager.MdiActiveDocument
Dim ed As Editor = doc.Editor
Try
Dim selectionRes As PromptSelectionResult
selectionRes = ed.SelectImplied
' If there's no pickfirst set available...
If (selectionRes.Status = PromptStatus.Error) Then
' ... ask the user to select entities
Dim selectionOpts As PromptSelectionOptions
selectionOpts = New PromptSelectionOptions
selectionOpts.MessageForAdding = _
vbLf & "Select objects to list: "
selectionRes = ed.GetSelection(selectionOpts)
Else
' If there was a pickfirst set, clear it
ed.SetImpliedSelection(Nothing)
End If
' If the user has not cancelled...
If (selectionRes.Status = PromptStatus.OK) Then
' ... take the selected objects one by one
Dim tr As Transaction = _
doc.TransactionManager.StartTransaction
Try
Dim objIds() As ObjectId = _
selectionRes.Value.GetObjectIds
For Each objId As ObjectId In objIds
Dim obj As Object = _
tr.GetObject(objId, OpenMode.ForRead)
Dim ent As Entity = _
CType(obj, Entity)
' This time access the properties directly
ed.WriteMessage(vbLf + "Type: " + _
ent.GetType().ToString)
ed.WriteMessage(vbLf + " Handle: " + _
ent.Handle().ToString)
ed.WriteMessage(vbLf + " Layer: " + _
ent.Layer().ToString)
ed.WriteMessage(vbLf + " Linetype: " + _
ent.Linetype().ToString)
ed.WriteMessage(vbLf + " Lineweight: " + _
ent.LineWeight().ToString)
ed.WriteMessage(vbLf + " ColorIndex: " + _
ent.ColorIndex().ToString)
ed.WriteMessage(vbLf + " Color: " + _
ent.Color().ToString)
' Let's do a bit more for circles...
If TypeOf (obj) Is Circle Then
' Let's do a bit more for circles...
Dim circ As Circle = CType(obj, Circle)
ed.WriteMessage(vbLf + " Center: " + _
circ.Center.ToString)
ed.WriteMessage(vbLf + " Radius: " + _
circ.Radius.ToString)
End If
obj.Dispose()
Next
' Although no changes were made, use Commit()
' as this is much quicker than rolling back
tr.Commit()
Catch ex As Autodesk.AutoCAD.Runtime.Exception
ed.WriteMessage(ex.Message)
tr.Abort()
End Try
End If
Catch ex As Autodesk.AutoCAD.Runtime.Exception
ed.WriteMessage(ex.Message)
End Try
End Sub
End Class
End Namespace
And here it is in C#:
using Autodesk.AutoCAD;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
namespace SelectionTest
{
public class PickfirstTestCmds
{
// Must have UsePickSet specified
[CommandMethod("PFT", CommandFlags.UsePickSet |
CommandFlags.Redraw |
CommandFlags.Modal)
]
static public void PickFirstTest()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
try
{
PromptSelectionResult selectionRes =
ed.SelectImplied();
// If there's no pickfirst set available...
if (selectionRes.Status == PromptStatus.Error)
{
// ... ask the user to select entities
PromptSelectionOptions selectionOpts =
new PromptSelectionOptions();
selectionOpts.MessageForAdding =
"\nSelect objects to list: ";
selectionRes =
ed.GetSelection(selectionOpts);
}
else
{
// If there was a pickfirst set, clear it
ed.SetImpliedSelection(new ObjectId[0]);
}
// If the user has not cancelled...
if (selectionRes.Status == PromptStatus.OK)
{
// ... take the selected objects one by one
Transaction tr =
doc.TransactionManager.StartTransaction();
try
{
ObjectId[] objIds =
selectionRes.Value.GetObjectIds();
foreach (ObjectId objId in objIds)
{
DBObject obj =
tr.GetObject(objId, OpenMode.ForRead);
Entity ent = (Entity)obj;
// This time access the properties directly
ed.WriteMessage("\nType: " +
ent.GetType().ToString());
ed.WriteMessage("\n Handle: " +
ent.Handle.ToString());
ed.WriteMessage("\n Layer: " +
ent.Layer.ToString());
ed.WriteMessage("\n Linetype: " +
ent.Linetype.ToString());
ed.WriteMessage("\n Lineweight: " +
ent.LineWeight.ToString());
ed.WriteMessage("\n ColorIndex: " +
ent.ColorIndex.ToString());
ed.WriteMessage("\n Color: " +
ent.Color.ToString());
// Let's do a bit more for circles...
Circle circ = obj as Circle;
if (circ != null)
{
ed.WriteMessage("\n Center: " +
circ.Center.ToString());
ed.WriteMessage("\n Radius: " +
circ.Radius.ToString());
}
obj.Dispose();
}
// Although no changes were made, use Commit()
// as this is much quicker than rolling back
tr.Commit();
}
catch (Autodesk.AutoCAD.Runtime.Exception ex)
{
ed.WriteMessage(ex.Message);
tr.Abort();
}
}
}
catch(Autodesk.AutoCAD.Runtime.Exception ex)
{
ed.WriteMessage(ex.Message);
}
}
}
}
You'll notice the copious use of ToString - this just saves us having to get (and print out) the individual values making up the center point co-ordinate, for instance.
Let's see this running with entities selected from one of AutoCAD's sample drawings. Notice the additional data displayed for the circle object:
Command: PFT
Select objects to list: 1 found
Select objects to list: 1 found, 2 total
Select objects to list: 1 found, 3 total
Select objects to list: 1 found, 4 total
Select objects to list:
Type: Autodesk.AutoCAD.DatabaseServices.Circle
Handle: 1AB
Layer: Visible Edges
Linetype: Continuous
Lineweight: LineWeight035
ColorIndex: 179
Color: 38,38,89
Center: (82.1742895599028,226.146274397998,0)
Radius: 26
Type: Autodesk.AutoCAD.DatabaseServices.Line
Handle: 205
Layer: Visible Edges
Linetype: Continuous
Lineweight: LineWeight035
ColorIndex: 179
Color: 38,38,89
Type: Autodesk.AutoCAD.DatabaseServices.BlockReference
Handle: 531
Layer: Dimensions
Linetype: ByLayer
Lineweight: ByLayer
ColorIndex: 256
Color: BYLAYER
Type: Autodesk.AutoCAD.DatabaseServices.Hatch
Handle: 26B
Layer: Hatch
Linetype: Continuous
Lineweight: LineWeight009
ColorIndex: 179
Color: 30,30,71
Command:

Subscribe via RSS
Hi, Kean!
I think that more elegant in C# is using:
Circle circ = obj as Circle;
instead of:
Circle circ = (Circle) obj;
and checking then:
if (circ != null) {
...
}
What do you think about that?
Posted by: Alexander Rivilis | September 25, 2006 at 02:36 PM
Of course, this is the completely WRONG way to do this. You should be using "as" as suggested in the previous comment, or at a minimum "is" followed by the cast.
Not as good as "as", but still way better, alternative:
if(obj is Circle)
circ = (Circle)obj;
There is almost NEVER a good reason to use
catch(System.InvalidCastException). Even lazyness isn't an excuse, because it's actually more typing.
Posted by: Wyatt | September 25, 2006 at 06:10 PM
Thanks for the various comments.
I'm sorry the C# code was incorrect - I was (embarassingly enough) not aware of "As". I do most of my coding in C++ or VB.NET, and wanted to include C# code for completeness (but didn't have the time to research the best way to do a typesafe cast in C#: the syntax of the C++ hardcast worked and threw an exception I could handle, so I assumed that was the way it was done. My apologies for not being as thorough as I might have been. I imagine the VB.NET code should also be revised, based on this feedback - if someone wants to post a better technique in these comments, then please do so.
I think this is telling me I should be taking a proper break during the rest again next week.
Thanks to everyone who posted or emailed their congratulations on Zephyr's birth - very much appreciated!
Regards,
Kean
Posted by: Kean | September 25, 2006 at 06:27 PM
OK - I went and revised the code in this posting (I hate being "revisionist" about these things, but I don't want to mislead any more readers about the right way to do things... I have left the original code there, with a comment).
BTW - one of the reasons I initially avoided the use of TypeOf() in VB.NET for this example was the suspicion that it wouldn't work with intermediate base classes, such as Autodesk.AutoCAD.DatabaseServices.Curve - mainly because the syntax makes it feel as though you're looking for the ultimate concrete class that the object belongs to. As it happens, this fear was ill-founded - TypeOf() works just fine for intermediate classes, so this is looking like a good topic for a future post.
Kean
Posted by: Kean | September 25, 2006 at 08:39 PM
An even better way perhaps:
Circle pCirc = tr.GetObject(objId, OpenMode.ForRead) as Circle;
if (pCirc != null)
// Do some mojo here...
Posted by: Glenn R | September 26, 2006 at 03:51 AM
If you're using the TypeOf ... Is ... type checking in VB.NET to verify the type before hand, you may want to consider using DirectCast instead of CType, as this has slightly less overhead. DirectCast translates directly to IL casting instuctions where CType calls a VB helper function first.
Posted by: Wyatt | September 26, 2006 at 04:57 PM
Really a very good article mite. You can simplify the task and bring it to a few lines if you use DOTNETARX. Its a library for doing common task such as openning the current database aand all for you. Here is a way of accessing the properties. I just gave the main part of the code :
*****************************8
try
{
ObjectId id1;
//get the editor for the current document
Editor ed = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument.Editor;
//promt user to select an entity and get its object id
PromptEntityResult res = ed.GetEntity(new PromptEntityOptions("Select an Entity"));
id1 = res.ObjectId;
DBObject obj = Tools.GetDBObject(id1);
Entity ent = (Entity)obj;
string color = ent.Color.ToString();
MessageBox.Show(color); }
}
catch (System.Exception)
{
}
*************************************88
in the above code example i am using a library DOTNETARX which is availabe free in the net. The Tools Class is available in that which helps you get a DBObject from an object id without having to write codes for transaction , openning database and all.
Posted by: Saraf Uddin Talukder | April 02, 2007 at 09:17 AM
I was wondering if you could possibly help with a liitle project I/m working on. What I would like to do is create a new layer and set the properties for that layer. I've figured out how to create the layer and set the color and lineweight, but I can't figure out how to set the linetype. I'm loading all the linetypes so I know they exist. Here's the code I have so far.... Any help would be greatly appreciated!!!
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.Runtime
Imports Autodesk.AutoCAD.ApplicationServices
Imports DBTransMan = Autodesk.AutoCAD.DatabaseServices.TransactionManager
Imports Autodesk.AutoCAD.Colors
Imports Autodesk.AutoCAD.Interop
Imports Autodesk.AutoCAD.Interop.Common
Public Class Commands
'Public db As Database = Application.DocumentManager.MdiActiveDocument.Database
' Define command 'importLS'
_
Public Sub importLS()
Dim ThisApp As AcadApplication = GetObject(, "AutoCAD.Application.17")
Dim ThisDrawing As AcadDocument = ThisApp.ActiveDocument
Dim oLSM As AcadLayerStateManager
oLSM = ThisDrawing.Application. _
GetInterfaceObject("AutoCAD.AcadLayerStateManager.17")
oLSM.SetDatabase(ThisDrawing.Database)
' If the drawing you're importing to does not contain
' all the linetypes referenced in the saved settings,
' an error is returned. The import is completed, though,
' and the default linetype is used.
On Error Resume Next
oLSM.Import("P:\CAD\Test\Architectural.las")
If Err.Number = -2145386359 Then
' Error indicates a linetype is not defined
MsgBox("One or more linetypes specified in the imported " + _
"settings is not defined in your drawing")
End If
On Error GoTo 0
End Sub
Posted by: Steve | October 10, 2007 at 11:04 PM
A few comments:
The code you've implemented uses COM to access the layer state manager. It'd be much simpler to use Database.LayerStateManager to access it, and import/export layer states.
But I actually think you're using the wrong approach: layers states are intended to save the current state of layers that exist in a drawing - they are really intended to be loaded into drawings that have the base layers already, and will simply influence the current state of those layers. To create new layers you'd be better off doing so by accessing the layer table and adding new entries.
I hope this helps,
Kean
Posted by: Kean | October 11, 2007 at 10:44 AM
I hate to think I already know the answer to this, but I'll ask anyway... Can the Implied Selection be filtered easily?
I have a function which requires that the user select Blocks with attributes, and in the normal selection methods that's easy enough to accomplish but if I am to enable the Pickfirst set for this function I need to pass the pickfirst through a filter, or loop through and check everything myself, which would require opening each object...
Posted by: David Osborne | November 15, 2008 at 12:30 AM
There's no way (I know of) to filter the pick-first set as it's being selected, but you can certainly pre-process the selection to remove the ones you don't like.
I don't see any way to avoid opening each object to check on whether it's a block containing attributes, however.
Kean
Posted by: Kean | November 16, 2008 at 02:22 PM
I was afraid of that...
I wasn't thinking of filtering the set 'as it's being selected', but rather immediately after, when my command is called and I obtain the Set. I was hoping for something like SelectionSet.FilterBy(SelectionFilter)
But I guess I'll just have to write my own FilterSelectionSet(SelectionSet, Filter).
Thanks anyway!
Posted by: David Osborne | November 17, 2008 at 07:30 PM
Wow, trying to do that filtering in my own code was going to get really complicated!
(Not too bad for the blocks w/attributes example I gave you, but really bad if you wanted to write it such that it would work no matter what you passed as a valid filter)
So I used a selection Function I already had which returns a selection set of all objects in ModelSpace which match a filter, then I compared my Implied Selection to the AllMatching selection and created a new ObjectIDCollection containing all ObjectIDs that appear in both selections.
Posted by: David Osborne | November 17, 2008 at 10:16 PM
Hi Kean,
I thought I'd make you aware of something I figured out recently...
I saw in your post about Object Specific Context Menu's that you used GetSelection to consume the implied selection or prompt if there was none.
Now, I do have a couple of cases where the way my code is organized that won't work for me, because I want to check at the beginning for an implied selection, but not prompt them if there is none because I'm about to display a form with some options and a Select button.
That said, in the cases where the GetSelection approach is appropriate, you can pass both the promptoptions and the filter and it will filter the implied selection if there is one.
Posted by: David Osborne | December 16, 2008 at 07:48 PM
GetSelection is a shortcut: you can certainly use the longhand approach if that's what you need.
Kean
Posted by: Kean Walmsley | December 21, 2008 at 07:40 PM
Hi kean,
I have created a new layer and add a line from c#. I dont want the user to select the line.(i.e if I select the line it should not get selected).I dont want ot lock the layer to do that.
Any code will be more helpful.
Regards,
sham
Posted by: sham | May 30, 2009 at 01:49 PM
Hi sham,
It depends whether you are asking the user to select objects from your own application or want to control this for standard commands.
I know from another comment that you've already found this post, which is of use if managing your own selection.
For standard commands you may want to investigate hooking into the selection process via the PromptForEntityEnding and PromptForSelectionEnding events.
Regards,
Kean
Posted by: Kean Walmsley | June 02, 2009 at 03:01 PM