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



    « A quick announcement... | Main | Some background to AutoCAD's MDI implementation and per-document data »

    September 25, 2006

    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:

    TrackBack

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

    Listed below are links to weblogs that reference Working with specific AutoCAD object types in .NET:

    Comments

    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?

    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.

    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

    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

    An even better way perhaps:

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

    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.

    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.

    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

    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

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

    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

    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!

    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.

    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.

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

    Kean

    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

    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

    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