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:

18 responses to “Working with specific AutoCAD object types in .NET”

  1. Alexander Rivilis Avatar

    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?

  2. 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.

  3. 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

  4. 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

  5. An even better way perhaps:

    Circle pCirc = tr.GetObject(objId, OpenMode.ForRead) as Circle;
    if (pCirc != null)
    // Do some mojo here...

  6. 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.

  7. Saraf Uddin Talukder Avatar
    Saraf Uddin Talukder

    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.

  8. 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'
    <commandmethod("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

  9. 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

  10. 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...

  11. 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

  12. 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!

  13. 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.

  14. 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.

  15. GetSelection is a shortcut: you can certainly use the longhand approach if that's what you need.

    Kean

  16. 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

  17. Kean Walmsley Avatar

    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

  18. Thanks for the code,

    It were a very good to concrete my script.

    Regards,

Leave a Reply to Saraf Uddin Talukder Cancel reply

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