Sunday, May 01, 2005

Class 12 - Deriving from AcDbObject

Introduction

I will start with Custom Objects and then proceed with custom entities on the next class. Custom objects can be used for several purposes and they are very powerful. Once your application creates and manage a custom object you will be able to construct complex application structures as well as much more intelligent and efficient data storage.

Starting with a simple example, suppose that you will need to build an ObjectARX application which implements some bars that has a length property and several types of shapes. Is possible that more than one bar has the same shape and it would be nice if you can provide a single instance of shape's information and share it among all bars using this shape.

The first impulse is to repeat the information on each bar no matter you will duplicate information. This works but will generate additional problems beyond the first problem which is the unnecessary space used to store the same information. Suppose that you need to update the shape and you would affect all bars that are using this shape. If your information is repeated in all bars you will need to open each bar and update its information. In other hand, if shape's information is stored into one single place and bars reflect this information you will need only to update this information in one place and all bars using this shape will be updated as soon as you update the shape's information.

AutoCAD use this technique on several features like layers, text styles, groups, etc. You will use exactly a custom object to store this information and share it, through its ObjectId, among all "clients" of this object.

How to begin

As discussed on previous class, you will need to derive from AcDbObject to be able to build your own custom object. This can be easily done using the ARXWizard or you can do by yourself creating the class by hand.

After create the class you need build some methods to create, store and acquire those objects instances from its container. Well, but where you should store your custom objects? AutoCAD provides a general purpose container called Named Object Dictionary (NOD). NOD is capable to store and persist any custom object derived from AcDbObject. It uses a dictionary like storage structure where you put a unique key (at the same level) and an object instance through its pointer and ObjectId. There are other custom object containers like Extension Dictionary that I will avoid due our course audience.

The NOD container could (and should) be organized by folders to make your dictionary as much organized as you can. The first node should be your application name to avoid conflict with other third-party ObjectARX applications that could use the NOD at the same time as you. The second level should contain all your business groups of objects. This will really depend on how many and the number of custom object types you have. NOD does not prohibit you to stored different classes at the same level but I really recommend you to avoid this except in case you need to stored generic objects together like on a Preferences group of objects.

You don't need to always Open and Close the NOD and go deep to find where are your desired object every time you need to access it. You can build some kind of cache of the most used Objects through its ObjectId and manage this cache to be updated for every single opened drawing. Remember that NOD is part of AcDbDatabase object and it is per document. So, you need to care about to build and fill your dictionary for every brand new drawing.

How to persist your custom objects

As I said before, the most used place to store custom objects is the NOD which is an AcDbDictionary. NOD takes care of its child objects because it is a container. So, when the AcDbDatabase object is issued to save its data by AutoCAD it also pass this message to its child objects and NOD is one of them. Once NOD receives this message it walks through its structure and call dwgOutFields() for every object stored there. The same process occurs when you open the drawing and the dwgInFields() is called by AcDbDatabase on NOD and consequently on its children. Exactly due that you will need to override the DWG filling methods to make possible to persist your custom objects among DWG open/close sessions.

Essential functions to override in your custom object class are:

virtual Acad::ErrorStatus dwgInFields(AcDbDwgFiler* filer);
virtual Acad::ErrorStatus dwgOutFields(AcDbDwgFiler* filer) const;
virtual Acad::ErrorStatus dxfInFields(AcDbDxfFiler* filer);
virtual Acad::ErrorStatus dxfOutFields(AcDbDxfFiler* filer) const;

If you don't plan to support DXF interface to your custom object you could avoid them.

Object's state management

On class 5 we have talked about object states when opening objects. Inside your custom object class you need to pay attention to call the proper assert method to make sure that all proper events and processes are fired when your object's state has changed. This is very important!

Those functions who change your object's data state must first call the assertWriteEnabled() function and then apply the required modifications. Functions who only read information from your object and does not affect its data state must call assertReadEnabled() function and also I really recommend that you make all these as const functions. This will avoid you to accidentally change the object's state when it is opened for read what will cause an assert error message. If you forget to call the proper assert method strange things may occur like call UNDO and your object stay unchanged and a lot other bizarre things.

How to create a custom object

To implement your custom object you will need to do the following:

1- Derive from AcDbObject;
2- Implement your data;
3- Implement access functions (read/write) with proper assert calls;
4- Implement the filling methods persisting and reading your data;

As a baseline, I will present a short example here:

// -------------------------------------------
// Class declaration
// -------------------------------------------

class MyClass : public AcDbObject {


public:
ACRX_DECLARE_MEMBERS(MyClass);

MyClass() {};
virtual ~MyClass() {};

Acad::ErrorStatus getVal (int& val) const;
Acad::ErrorStatus setVal (int val);

Acad::ErrorStatus getString (CString& str) const;
Acad::ErrorStatus setString (LPCTSTR str);

virtual Acad::ErrorStatus dwgInFields(AcDbDwgFiler*);
virtual Acad::ErrorStatus dwgOutFields(AcDbDwgFiler*) const;

private:
int m_Val;
CString m_Str;

};

// -------------------------------------------
// Class Definition
// -------------------------------------------

ACRX_DXF_DEFINE_MEMBERS(MyClass,
AcDbObject, AcDb::kDHL_CURRENT,
AcDb::kMReleaseCurrent, 0, MYCLASS, MYSAMP);

// -------------------------------------------
Acad::ErrorStatus MyClass::getVal (int& val) const {

assertReadEnabled();
val = m_Val;
return Acad::eOk;

}
// -------------------------------------------
Acad::ErrorStatus MyClass::setVal (int val) {

assertWriteEnabled();
m_Val = val;
return Acad::eOk;

}
// -------------------------------------------
Acad::ErrorStatus MyClass::getString (CString& str) const {

assertReadEnabled();
str.Format(_T("%s"),m_Str);
return Acad::eOk;

}
// -------------------------------------------
Acad::ErrorStatus MyClass::setString (LPCTSTR str) {

assertWriteEnabled();
m_Str.Format(_T("%s"),str);
return Acad::eOk;

}
// -------------------------------------------
Acad::ErrorStatus MyClass::dwgInFields(AcDbDwgFiler* pFiler) {

assertWriteEnabled();
AcDbObject::dwgInFields(pFiler);

Adesk::Int16 _val = 0;
pFiler->readInt16(&_val);
m_Val = _val;
TCHAR* _temp = NULL;
pFiler->readString(&_temp);
m_Str.Format(_T("%s"),_temp);
acutDelString(_temp);

return pFiler->filerStatus();

}
// -------------------------------------------
Acad::ErrorStatus MyClass::dwgOutFields(AcDbDwgFiler* pFiler) const {

assertReadEnabled();
AcDbObject::dwgOutFields(pFiler);

pFiler->writeInt16(m_Val);
pFiler->writeString(static_cast<const TCHAR*>(m_Str));

return pFiler->filerStatus();

}
// -------------------------------------------

// -------------------------------------------
// Entry Point
// -------------------------------------------
AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* appId) {
switch (msg) {
case AcRx::kInitAppMsg:

acrxDynamicLinker->unlockApplication(appId);
acrxDynamicLinker->registerAppMDIAware(appId);

MyClass::rxInit();
acrxBuildClassHierarchy();

break;

case AcRx::kUnloadAppMsg:

deleteAcRxClass(MyClass::desc());

break;
}

return AcRx::kRetOK;
}

How to create and store your custom object

The NOD container is based on AcDbDictionary class which has several methods to read, write and erase entries. Your application needs to take care of NOD entries and be responsible to create instances of your custom class and store these objects inside the NOD. Each object stored must have a key defined or a generic key using the star * as its name.

void createMyObjects() {

AcDbDictionary *pNamedobj = NULL;
acdbHostApplicationServices()->workingDatabase()->
getNamedObjectsDictionary(pNamedobj, AcDb::kForWrite);

AcDbDictionary *pDict = NULL;
if (pNamedobj->getAt(_T("MYDICT"),(AcDbObject*&) pDict,
AcDb::kForWrite) == Acad::eKeyNotFound) {

pDict = new AcDbDictionary;
AcDbObjectId DictId;
pNamedobj->setAt(_T("MYDICT"), pDict, DictId);

}

pNamedobj->close();

if (pDict) {

MyClass *pObj1 = new MyClass();

pObj1->setVal(1);
pObj1->setString(_T("String1"));


MyClass *pObj2 = new MyClass();

pObj2->setVal(2);
pObj2->setString(_T("String2"));


AcDbObjectId rId1, rId2;

pDict->setAt(_T("*M"),pObj1, rId1);
pDict->setAt(_T("*M"),pObj2, rId2);


pObj1->close();
pObj2->close();


pDict->close();

}
}

How to verify if my objects are really stored inside NOD?

You will need to iterate the NOD entries to find your dictionary and then perform an iteration over its entries. The process should be something like this:

void listMyObjects() {

AcDbDictionary *pNamedobj = NULL;
acdbHostApplicationServices()->workingDatabase()
->getNamedObjectsDictionary(pNamedobj, AcDb::kForRead);

AcDbDictionary *pDict = NULL;
pNamedobj->getAt(_T("MYDICT"), (AcDbObject*&)pDict,AcDb::kForRead);
pNamedobj->close();

if (pDict == NULL) {

acutPrintf(_T("\nThe dictionary MYDICT does not exist. Please create it first!"));
return;

}

AcDbDictionaryIterator* pDictIter= pDict->newIterator();

MyClass *pMyClass;
int _val;
CString _str;

for (; !pDictIter->done(); pDictIter->next()) {

pMyClass = NULL;
pDictIter->getObject((AcDbObject*&)pMyClass,AcDb::kForRead);

if (pMyClass != NULL) {

pMyClass->getVal(_val);
pMyClass->getString(_str);
pMyClass->close();
acutPrintf(_T("\nMyClass: val=%d, str=%s"),_val,_str);

}

}

delete pDictIter;
pDict->close();

}

Stay tuned for the next Lab which will require you to build a custom object. See you there!

13 comments :

Anonymous said...

Thank you, Fernando. It is really a great tutorial.
I am going to build a small program which can retrieve some Attribute values from title block like dwgtitle or proj no. It will be running on engineering computers without AutoCAD Enviorment. I think ObjectARX can do this. Could you give some hints?

Thanks,
Sharon

Fernando Malard said...

Hi Sharon,

Could you contact me at fpmalard@yahoo.com.br ?

Thank you.
Fernando.

Anonymous said...

Ok but I have a question....

I understand that ARX Wizard can create these dwn IN/OUT functions. And also it automatically implements any variable which is integer, double, CString(after some slight modification manually) etc... Very nice...

But what if we use other types of objects like AutoCADs own classes or our own classes.How we will save them, they cannot be saved with the same procedure as if it is an integer(I mean the standard "....pFiler->writeString........pFiler->writeItem ....." functions created for numbers and strings created automatically inside dwg IN/Out functions by ARX Wizard.

If we use one class in another, I see that these functions automatically created by ARXWizard gives error.And it only works when I erase these functions(Means no dwg IN/OUT functions for some variables especially my own classes).And propably not saved inside the database. or am I wrong?

Thanks.

Anonymous said...

by the way here we can see very nice code written by Fernando which overrides those created by ARXWizards.

I wish I can write my ARX code without using ARX wizards one day.

Without ARXWizard or Fernando unexperienced programmers like me are DOOMED :)))))))

Fernando Malard said...

Hi Sharon,

ObjectARX can only be executed inside AutoCAD, AutoCAD verticals or AutoCAD OEM. If you need an application which deals with DWG Databases without AutoCAD you will need to use the RealDWG solution from AutoCAD.

Regards,
Fernando.

Fernando Malard said...

Hello Mazhar,

There are basically 3 situations to consider here:

1) You have a pure C++ class which you have embedded into your custom object/entity using a member or a pointer. In this case I prefer to create the dwgIn/Out methods inside it and call if from inside my custom object when its own dwgIn/Out is called. The problem with this method is that you can not put this class into a non-ObjectARX DLL due the classes it uses.

2) The second option, which I recommend, is when you have a class derived from AcRxObject. If you use both the ACRX_DECLARE_MEMBERS() and ACRX_CONS_DEFINE_MEMBERS() macros your class will be non-database resident. This is a recommended approach because you can take advantage of RTTI(runtime type identification) and ARX class factory (isA, desc, create, etc). In this situation you may use the same approach as in 1, create your own dwgIn/Out methods and call them from inside your custom objects.

3) Your custom object has an embedded native ObjectARX class. Generally in this case you have a pointer inside your class pointing to this embedded object. Inside your class constructor you instantiate it and on destructor you delete it. When your custom object's dwgIn/Out is called you just need to forward the call using the stored pointer. Suppose you have an embedded AcDbLine through a pLine pointer variable. Inside your dwgInFields() you just need to do:

if (pLine) pLine->dwgInFields(pFiler);

The main idea on this 3 approaches is to keep things organized and establish the behavior of each object without creating a caos into your code.

Hope this help,
Fernando.

Pushpak Phule said...

hi fernando,
Very nice work in this blog.
I am using objecARX 2007 and need to use solprof and soldraw for viewports generated. Is there any API available for the same??

regards,
Pushpak

Fernando Malard said...

Hi pushpak,

I'm afraid you will need to use their native command. As far as I know there is no API for them.

The solprof and soldraw commands seems are placed into acsolids.arx module.

Try something like (not tested):

[CODE]



if (acedArxLoad("ACSOLIDS.ARX") != RTERROR)
{
// Select your solids
ads_name sset;
acedSSGet(NULL, NULL, NULL, NULL, sset);

struct resbuf *pArg;
struct resbuf *pRes;

pArg = acutBuildList(RTSTR,"c:solprof",RTPICKS,sset,RTSTR,"Y",RTSTR,"Y",RTSTR,"Y",0);

if (pArg)
{
acedInvoke(pArg, &pRes);
acutRelRb(pArg);
acutRelRb(pRes);
}

acedSSFree(sset);
}



[/CODE]

Please let me know if this works and solve your problem.

Regards,
Fernando.

Anonymous said...

Hi Fernando,
I have derived a custom Point from AcdbPoint. I have AcDbHardOwnershipId as member to store the ObjectId of line on which to point lies.
I am unable to copy the custom point. On going through your blog I got some hints on implementing copyFrom. But i did not get any idea on implementing it.
I dont have any problem on saving and retrieving the custom point stuffs. Got problem in pasting it. I would be glad to know if there is any sample dealing with copy paste implementation.
Thank you.

Anonymous said...

When i tested with hardPointerid, instead of ownershipid, i didnot get problem.
the theory of ownership and pointerid helped me.
Thanks

Fernando Malard said...

Hi,

It is essential to use the correct approach because an Ownership works totally different than a Pointer relation.

It is always a good idea to mimic AutoCAD database relationships like Layer <> Entity, BTR <> Entity, Group <> Entity, etc.

Glad it worked!
Regards,

Anonymous said...

Hi Fernando hope you are still around.

I have this oldschool function code .
Everything seems good but in the very next line of code I'm getting this error Unhandled exception at 0x000007feeab023f0 in acad.exe: 0xC0000005: Access violation reading location 0x0000000000000010..

void CRectWindow::addWindowInfo(CRectWindow *pWindowObj)
{
AcDbDatabase *pCurDb;
AcDbObject *pObj;
AcDbObjectId dicObjId, entId, windowObjId;
AcDbDictionary *pNamedObjD = NULL;
AcDbDictionary *pDict = NULL;

ads_name ename;
AcDbHandle handEnt;
TCHAR strHandle[17];

pCurDb = acdbHostApplicationServices()->workingDatabase();
pCurDb->getNamedObjectsDictionary(pNamedObjD, AcDb::kForWrite);

//Check to see if the dictionary we want to create is already present if it's not then create it and add
if(pNamedObjD->getAt(_T("WINDO_INFO"), (AcDbObject*&) pDict, AcDb::kForWrite) == Acad::eKeyNotFound)
{
pDict = new AcDbDictionary;
pNamedObjD->setAt(_T("WINDO_INFO"), pDict, dicObjId);
}

pNamedObjD->close();


// Get the last entity
acdbEntLast(ename);
// Get the entity object ID
acdbGetObjectId(entId, ename);

// Open the enity for a read operation
acdbOpenObject(pObj, entId, AcDb::kForRead);

pObj->getAcDbHandle(handEnt);
handEnt.getIntoAsciiBuffer(strHandle);
pObj->close();

// Open the dictionary for a write operation
acdbOpenObject(pDict, dicObjId, AcDb::kForWrite);
pDict->setAt(strHandle, pWindowObj, windowObjId);
pDict->close();

}

what would it be problem(s) for your opinion thanks.
Best Regards.

Fernando Malard said...

Hello,

It seems the problem is related to dicObjId variable.
This variable is not initialized when you don't create the WINDO_INFO dictionary.

I would change the initial code to:

------
if(pNamedObjD->getAt(_T("WINDO_INFO"), (AcDbObject*&) pDict, AcDb::kForWrite) == Acad::eKeyNotFound)
{
pDict = new AcDbDictionary;
pNamedObjD->setAt(_T("WINDO_INFO"), pDict, dicObjId);
}
else
dicObjId = pDict->objectId();

------

Further, you should always check if an object opening was succeeded before continue. So all your acdbOpenObject() calls should be comparing the result to Acad::eOk.

So I would change the last portion to:
------

// Open the enity for a read operation
if (acdbOpenObject(pObj, entId, AcDb::kForRead) == Acad::eOk)
{
pObj->getAcDbHandle(handEnt);
handEnt.getIntoAsciiBuffer(strHandle);
pObj->close();

// Open the dictionary for a write operation
if (acdbOpenObject(pDict, dicObjId, AcDb::kForWrite) == Acad::eOk)
{
pDict->setAt(strHandle, pWindowObj, windowObjId);
pDict->close();
}
}

------

Hope this will fix your code.

Regards,