Sunday, April 24, 2005

Class 11 - Custom ObjectARX Class

Introduction

In my personal opinion, the greatest feature of ObjectARX is the capability to develop your own objects and entities. This powerful feature will allow you to create complex applications and provide your users a unique experience using your software.

This feature is possible since ObjectARX beginning (officially at AutoCAD R13). Since Autodesk provided this feature, its own developers thought that they could develop their selves vertical solutions for the most interesting market areas. At that time, products like MAP, MCAD and ADT start to show up.

Today there are several vertical products based on AutoCAD made by Autodesk and much more developed by third-party companies.

How is this possible?

ObjectARX takes full advantage of C++ language features like inheritance, polymorphism and overriding among many others. This allows Autodesk to publish part of AutoCAD source code using a SDK like package with library and header files.

Beyond this point, ObjectARX exports some classes allowing you to derive from them and implement your own behavior taking advantage of all ready-to-use methods and overriding those ones you need.

This way, when your custom class is defined and implemented your application will be compiled and linked with AutoCAD native libraries and headers and will be possible to load your module (DLL) at runtime to use your own objects inside AutoCAD! Great hum?

When to use and when not to use custom objects

Even being a powerful feature of ObjectARX, custom objects are not the best solution for any kind of implementation. Sometimes is better to user another solution like XData or XRecords. This will totally depend on how much complex your application is and how much complex will be the way users will interact with your product.

You need to be sure when to use or not custom classes inside ObjectARX. Personally I perform some questions to myself that will help me to decide:

- My product's elements are simple or complex?
- My elements are only complex in terms of non-graphical data or they will require complex graphical representation?
- Do I need to protect my element's data when the drawing is out of my company?
- Will my elements present a complex interaction with users very different from AutoCAD native entities?
- Do I need to share common information among my elements?

These questions will really help you to decide or not to user custom classes.

How to use a custom objects?

The first step is to choose your base class. This will depend on what type of custom object are you willing to implement. Basically you need to choose if it will represent an entity or a data object. If it will be an entity you will need to derive it from AcDbEntity or other of its derived classes. In other hand, if it will not have graphical appearance, you will derive it from AcDbObject. There are some ObjectARX classes that does not allow you to derive from. Take a look at ObjectARX documentation for a complete list.

It is very important to you clearly understand these differences between AcDbEntity and AcDbObject. Remember that every AcDbEntity if also an AcDbObject but NOT ALL AcDbObject is an AcDbEntity. This is because AcDbEntity is derived from AcDbObject.

I really would like you to walk through the ObjectARX class hierarchy to locate yourself in that tree and see clearly what are we talking about. There is a DWG file, called classmap.dwg, inside your ObjectARX SDK folder called \classmap.

Runtime Identification

AutoCAD requires that every custom class has its own runtime type identification. This will be used by AutoCAD and by your own application. Basically you don't need to care about this because there are MACROS to do this job for you. The AcRxObject class is the responsible to perform this feature and exactly due that it is on the top of AcRx tree.

The runtime identification is made by some functions like the following:
  • desc(), a static member function that returns the class descriptor object of a particular (known) class.
  • cast(), a static member function that returns an object of the specified type, or NULL if the object is not of the required class (or a derived class).

  • isKindOf() returns whether an object belongs to the specified class (or a derived class).
  • isA() returns the class descriptor object of an object whose class is unknown.
These functions are pretty useful because they help you to get runtime important information from AutoCAD native objects and your own objects. A good example if when you have a pointer to an AcDbEntity and would like to know if it is an AcDbCircle or an AcDbLine. How? Just use the above functions to get the information or even try to cast the pointer.

To declare these functions you will need to use the following MACRO inside your class declaration:

ACRX_DECLARE_MEMBERS(CLASS_NAME);

To implement these functions, you will need to use one of the following MACROS:
  • ACRX_NO_CONS_DEFINE_MEMBERS (CLASS_NAME, PARENT_CLASS):
    Use for abstract classes and any other classes that should not be instantiated.
  • ACRX_CONS_DEFINE_MEMBERS (CLASS_NAME, PARENT_CLASS, VERNO):
    Use for transient classes that can be instantiated but are not written to file.
  • ACRX_DXF_DEFINE_MEMBERS (CLASS_NAME,PARENT_CLASS, DWG_VERSION, MAINTENANCE_VERSION, PROXY_FLAGS, DXF_NAME, APP):
    Use for classes that can be written to, or read from, DWG and DXF files.
Additionally, you will need to initialize and delete your custom class from ObjectARX runtime tree which can be done, during your application's kInitAppMsg and kUnloadAppMsg using the following functions implemented by the above MACROS:

// Inside kInitAppMsg function handler
MyClass::rxInit();
// Call this only once for all of your custom classes
acrxBuildClassHierarchy();

// Inside kUnloadAppMsg function handler
deleteAcRxClass(MyClass::desc());

This will guarantee that when your application is loaded AutoCAD recognizes your class and when it was not present (unloaded) AutoCAD will transform your class instances into Proxy entities. Proxy entities are a binary package that protects and preserves your custom object during DWG roundtrip without your application.


36 comments :

GIS Programer said...

it is very nice blog for objectArx beginners

Unknown said...

Hello

I am porting my application from DWGDirect lib to RealDWG lib and have a one problem which is releated for Custom entities (derived classes).

With DWGDirect library it possible to make PSEUDO database object (These object are overriding the original objects from which are derived).

IF you make your custom entity ExAcDbLine which you derived it from AcDbLine and register it for a Run TIme Ident., when you read DWG file, RealDWG library for LINE give you pointer to the EXAcDbLine, not to AcDbLine. This is possible to do with DWGDirect library (there are macros for do this:
ODDB_PSEUDO_DECLARE_MEMBERS
ODDB_PSEUDO_DEFINE_MEMBERS
) but in RealDWG there are no these macros for this thing.

"Pseudo classes (PSEUDO database objects) is method to substitute run-time class only to alter some behavior or to store additional run-time information. They are saved to file as their base class."

It is possible to have same thing in RealDWG as there is in DWGDirect library.

Regards
ErvinD

Fernando Malard said...

Ervin,

There are no MACROS to do that but you can play around with your class and tweak the runtime procedures to achieve the same results.

I don't have such a sample code but if you take a deep look into ObjectARX documentation about the Runtime classes.

Regards,

Unknown said...

Hello Fernando

Somewhere it must be replaced pointer to the RealDWG AcRxClass with my derived class pointer, i know that, but i do not have experience with these things, only i am good user of the RealDWG library !

Thank you anyway
Ervin

jandreasen said...

Hi Fernando,

I've been using AutoCad for a number of years and would like to learn ObjectARX. Where can I get Microsoft Visual C.NET 2005(8.0), I did a google search and found Microsoft Visual C++ 2005 Redistributable Package (x86) (is this the same?) Are there any books (for beginners)that you could recommend?

Thanks for creating this site.

Joe

Fernando Malard said...

Hi Joe,

You will need to buy a VC++ 2005 product from Microsoft. There is a free version called VC++ Express but it won't let you create resources.

The redistributable package is for runtime purposes only (deployment).

*ObjectARX 2007 and 2008 does not support Visual Studio Service Pack 1. You will need to use VC++ 2005 Retail version.

There are just a few books about ObjectARX, actually a couple at Amazon.com

Regards,

Unknown said...

hi

i have a bunch of custom objects etc and i want to expose them through the autocad VBA api. how can i do this? my objective is to provide and easy means for others to use my objects alongside the autocad objects in a vba routine. thanks

Fernando Malard said...

Hi Olivio,

You will need to add a COM wrapper exposing the properties/methods you want to call from VBA.

This is well documented inside ObjectARX Help at "Writing a COM Wrapper" topic.

Further, there is a sample which demonstrate that:
\ObjectARX 2007\samples\com\AsdkSquareWrapper_dg

Regards,

Anonymous said...

Hi Fernando,

obviously I must code the DBX-part of a custom object in C++. Can I write the ARX-part with e.g. C#/.NET ?

Thanks for your great blog
Mark

Fernando Malard said...

Hello Mark,

Custom entities are still available only through C++ even on 2010 but there is a new concept called overrule which allow you to tweak the native entities through a .NET layer.

If you need/want to use custom entities but make your ARX portion in .NET you will need to create a .NET wrapper to allow access to your custom class from .NET. This in fact is done by AutoCAD itself because the major part of its core is made still in pure C++ and the classes are exposed through .NET wrappers.

Take a look at your ARXWizard and you will find a .NET Wrapper wizard to allow a easy creation of the custom class .NET wrappers.

Good luck.

Anonymous said...

No luck with the buggy ObjectARXWizard.

I did the following:
1. I created an ObjectDBX with Extension DLL, MFC Support, COM Server using ATL, Import Acad COM Interfaces
2. I added a custom class poly1 derived from AcDbPolyline with automation support
3. I added an ObjectDBX ATL COM Wrapper Object DBX classname=poly1 with Entity interface support, everything else is default.
The wizard terminates with error: 'undefined' is Null or no Object.
I use VS2005 and ObjectARXWizard 6.0.1.0.
Can this be done manually.

Perhaps it might be an interesting idea to extend one of your labs with a COM wrapper.

Mark

Fernando Malard said...

Mark,

COM Wrapper and .NET Wrapper are not the same thing.

COM Wrapper is used to expose your class to be accessed via COM interface clients such as VBA.

.NET Wrapper will create a .NET class to allow your to manipulate your custom C++ class from .NET world.

Try to create a simple project, with a custom entity without any COM stuff then open ARX Class View, right click and select .NET Wrapper Wizard.

Regards.

Anonymous said...

I got it, but it was not easy. So I hope this will help other readers as well:

In my ObjectARX class explorer there is definitely no "Add a .NET wrapper".
I enabled it via editing C:\Program files\Autodesk\ObjectARX Wizards for AutoCAD 2008\ArxAddInWiz\resources\1033\ArxAddinWiz.xml
Add in ^ClassExplorer>^ContextMenu>^Project>" the following line:
^Entry wizard="vc\vcaddclass\ObjectARX\ArxNETWrapper.vsz">Add a .NET Wrapper...^/Entry>
You must restart VS that the change takes effect.

At first: you must create your ObjectDBX-project with ".NET mixed managed code support" otherwise you will get compilation errors.
Now we can right-click on "Add a .NET wrapper".
It is important to fill in the correct "Managed Base class". E.g. when your object derives from AcDbPolyline you must fill in Polyline.

You will still get compilation errors with GetImpObj. To resolve this you have to add to your StdAfx.h the line #include "yourclass.h".
Now you can view your class in the object-explorer.

Phew, that was hard work.

Mark

Fernando Malard said...

Mark,

That's great.
Thanks for the feedback.

Regards.

Mark said...

Further experimenting showed that the ObjectARX .NET wrapper gives you some more headaches. I found a better solution at Invoking Unmanaged Methods from AutoCAD's .NET API">.
You can apply this to your own DBX.

Fernando Malard said...

Mark,

PInvoke depend on the functions signature (decorated name) that may change in future releases.

For Custom Entities the best solution is to create the Wrapper once it will also expose to .NET only things you want and will respect the ObjectARX open/close mechanism, pointer allocation, etc.

If you still want to use Interop I would suggest a better article of my friend Kean Walmsley:

http://through-the-interface.typepad.com/through_the_interface/2006/07/calling_objecta.html

Regards.

Mark said...

Fernando,

I created a separate wrapper DLL for .NET. If I open this DLL with Reflector everything looks fine, but if I add the DLL to a VB.NET project I only see the underlying Polyline object and none of my class properties. If you wish, I can send you the DLL or the project.

Thanks for your effort.

Fernando Malard said...

Mark,

I see, but it will depend on how you will need to use this DLL.

If you create the Wrapper following the ARX guidelines it need to work with both C#, VB.NET or any other VS.NET language.

Please, let me know about your progress on that...thanks.

Mark said...

My wrapper header file:
namespace Test1 {

[Autodesk::AutoCAD::Runtime::Wrapper("APipeline")]
public __gc class BPipeline : public Autodesk::AutoCAD::DatabaseServices::Polyline
{

public:
BPipeline();
BPipeline(const AcString sHatch) ;

public private:
BPipeline(System::IntPtr unmanagedPointer, bool autoDelete);
inline APipeline* GetImpObj()
{
return static_cast<APipeline*>(UnmanagedObject.ToPointer());
}

public:
__property void set_Center(Point2d point);
__property Point2d get_Center();
bool GapInsert(const AcGePoint3dArray& points, const int distance);
};
}

In VB.Net I can do:
Dim pl1 as New Test1.BPipeline()
but
Dim pl1 as New Test1.BPipeline("Test2")
fails (constructor not available)
The center property is also available but not GapInsert

Fernando Malard said...

Mark,

What happens if you change your other constructor from:

BPipeline(const AcString sHatch);

to:

BPipeline(string sHatch);

Does it become accessible from VB.NET?

I think the problem is related to the parameters types because AcString is not recognized out of your mixed-mode class.

Regards.

Regards,
Fernando.

Mark said...

Fernando,

you are on the right track. string did not work, but
BPipeline(System::String* sHatch)
does. But how can I convert this to AcString ?
APlant::BPipeline::BPipeline(System::String* sHatch) :Autodesk::AutoCAD::DatabaseServices::Polyline(new APipeline(*sHatch), true) {} ?
The next headache will be my GapInsert function. I need to find an equivalent for AcGePoint3dArray&

You will already have noticed that I am not very familiar with C++.

Fernando Malard said...

Mark,

The problem is due the types you are using at your function's parameters.

You need to use .NET AutoCAD types instead of C++ types.

For example, does not use AcGePoint3d but use Point3d. Take a look at .NET arxmgd.chm documentation inside ObjectARX doc folder.

Change your method's parameter from AcGePoint3dArray to:

Autodesk::AutoCAD::Geometry::Point3dCollection.

From the doc:
"This .NET class wraps the AcGePoint3dArray ObjectARX class. Point3dArray is a template class that uses the AcArray class template."

It will do the trick.

Regards,
Fernando.

Mark said...

I am still stuck with (invalid conversion)
APlant::BPipeline::BPipeline(System::String* sHatch) :Autodesk::AutoCAD::DatabaseServices::Polyline(new APipeline(sHatch), true)

I tried
APlant::BPipeline::BPipeline(System::String* sHatch)
{
const wchar_t __pin* unmngStr = PtrToStringChars(sHatch);
new BPipeline(new APipeline(unmngStr), true);
}
This compiles, but gives rt message Forgot to call Dispose?

Fernando Malard said...

Mark,

You need to marshal the string. Take a look at the Marshal class and more closely to these two methods:

Marshal.PtrToStringAuto()
Marshal.StringToHGlobalAuto()

The "Auto" version of these methods will handle both ANSI and UNICODE scenarios accordingly.

More info here:
http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal_methods.aspx

Let me know if it did the trick.
Regards.

Mark said...

1. String. I got it working the following way:
a) In my dbx I changed APipeline(AcString sHatch) to APipeline(const wchar_t* sHatch)
b) In my wrapper I wrote
APlant::BPipeline::BPipeline(System::String* sHatch) :Autodesk::AutoCAD::DatabaseServices::Polyline(new APipeline(StringToWchar::StringToWchar(sHatch)), true)
StringToWchar is defined in mgdinterop.h

2. Point3dCollection
bool Test1::BPipeline::GapInsert(const Autodesk::AutoCAD::Geometry::Point3dCollection& points, const int distance)
{
return GetImpObj()->GapInsert(*reinterpret_cast<const AcGePoint3dArray*>(&(points)), distance);
}

But this does not compile. How should that be converted ?

Fernando Malard said...

Mark,

I'm afraid you will have to copy element by element once this parameter is an array and its element is also a complex type (a 3D point).

Try to copy point by point.

Regards.

Mark said...

Fernando,

with your help I got it, thanks again :) I hope this may help others as well

bool Test1::BPipeline::GapInsert(Autodesk::AutoCAD::Geometry::Point3dCollection& points, const double distance)
{
AcGePoint3dArray upta;
for (int i=0; i < points.Count; i++) upta.append(GETPOINT3D(points.get_Item(i)));
return GetImpObj()->GapInsert(upta, distance);
}

Fernando Malard said...

Mark,

That's great!
If you want, organize the information with the code fragments you built and I can publish it here with credits to you (send me your name, e-mail, etc). This way other people will find it easier.

Regards.

Mark said...

Fernando,

as I see your blog cuts the lines so some information is missing.
I want to test my solution a bit further to be quite sure that it is ok. At the moment some things simply work but I do not know why.

The motivation behind this is to reduce the quite unproductive and error-prone C++ to the minimum (i.e. the custom object DBX).
I hope that I do not need C++ any longer for the ARX part.
I also do not know whether the wrapper library will have performance impacts.

I could not find your email-adress. If you send it to lebakram1_at_gmx.net I will send you the routines which you can hopefully publish in a readable form.

Mark said...

Fernando,

is it possible to add a command to a dbx-module ? I would like to add a command which returns a versioninfo-string.
ACED_ARXCOMMAND_ENTRY does not compile in a dbx-module.

Fernando Malard said...

Hi Mark,

No, DBX modules can contain only non-AutoCAD interface stuff. If you add commands you will violate the DBX/ARX module rules.

Regards.

Mark said...

My custom object is derived from AcDbPolyline. If I select the object the property palette shows "Polyline" instead of "MyCustomObject". How can I achieve this ?

Thanks

Fernando Malard said...

Mark,

OPM dialog uses COM to access your entity's class information.

When you derive from a class different of AcDbEntity I guess the OPM use the COM class descriptor to display a name at OPM dialog. If you don't implement the COM interface it will look up the next parent class information which is AcDbPolyline in your case.

This seems to be a specific behavior and I'm not sure you can avoid this by implementing some COM interface methods for your custom class. You need to try to see what happens.

Have you tried to compile and test PolySamp which derives from AcDbPolyline too but implements a COM interface? Take a look and see what OPM display for it.

Regards.

Mark said...

Fernando,

you are probably right. I looked at polysamp and it displays the custom classname in the property palette. But polysamp has several thousand lines and I am not able to find out where the crucial information hides. Trying to adapt compoly to my custom object produced more than 100 errors and I have no idea what idl is & how it works. Probably it's not worth the effort.

CADventure said...

I got a drawing with Custom Entities and also got the Object Enablers. So now the entities show up in the drawing. Now is it possible to read the properties of the Custom Entities through .Net API ? I also need to read the position of the Custom Entity and need to dimension them.

My best guess is, the developer of the Custom Entity need to take care about exposing the properties so that those properties can be accessed by .Net API. Am I correct ? Now I don't have anything excepts the object enablers ( dbx ). How to proceed ?

Fernando Malard said...

Hi Subir,

You can only read through .NET what was exposed through C++ classes via .NET wrappers.
If the Custom Entity author didn't create a mixed-mode module exposing their classes and properties you won't be able to read from .NET anything beyond the AutoCAD base class level information. Thinks like Layer, Color, LineType, etc.

To hack DBX object enablers you would need to replicate all the class structure, their embedded objects, their properties and worse, figure out the sequence they were saved into the DWG once it is a binary sequential file.

Regards,