Sunday, March 06, 2005

Class 5 - Object Management

Introduction

On previous class we have talked about object lifecycle. On this class we will go further on how to manage AutoCAD objects. As I have mentioned every object has its own identification called ObjectId. This is the key for acquiring its pointer and perform read or write operations.

The standard access method is made by an OPEN operation (for write, for read or for notify), some operations and a CLOSE method. Another approach, much more efficient is through transactions. This mechanism is much more secure and efficient. Let's talk about both methods.

Using standard OPEN / CLOSE method

This is the most used method but, in other hand, is the most unsafe because you may forget to close the object and then cause access violations or even fatal errors that will cause AutoCAD to terminate.

The global standard function to open objects is called acdbOpenObject(). This function will open every object derived from AcDbObject class and will provide you a C++ pointer to access the object properties and methods. One of this function signatures is the following:

inline Acad::ErrorStatus acdbOpenObject(AcDbObject *& pObj, AcDbObjectId id, AcDb::OpenMode mode, bool openErased);

pObjOutput pointer to the opened object
idInput the object ID of the object to open
modeInput mode to open object
openErasedInput Boolean indicating whether it's OK to open an erased object

This function receives an empty pointer to AcDbObject by reference that will be filled out by AutoCAD if there is an object with the provided input variable id. Further you need to provide your action intention on the object which can be WRITE, READ or NOTIFY. The latest parameter indicate if you would like to open the object if it is on erased status. Remember we have talked that erased objects remain inside AutoCAD database until the next save operation.

The opening intention is very important because it will limit or not what you can do with the object. If you open an object for READ you will not be able to call methods that modify the object's state. In other hand, if you open the object for WRITE you will be able to both modify and read object data. Ah, so is better to always open the object for WRITE?

definitely NOT!

When you open an object for WRITE AutoCAD fires several pre and post procedures on it that causes some performance overhead. If your routine opens several objects and you use the WRITE flag you will certainly loose performance.

The same object can be open for READ up to 256 times without close but it is not recommended. You should always close the object as soon as possible. If an object is opened for WRITE you can't open it a second time for WRITE. Basically, you need to follow the following rules:

Opening objects in different modes
Object opened for:kForReadkForWritekForNotify
openedForReadeAtMaxReaderseWasOpenForRead(Succeeds)
openedForWriteeWasOpenForWriteeWasOpenForWrite(Succeeds)
openedForNotifyeWasOpenForNotifyeWasOpenForNotifyeWasOpenForNotify
wasNotifying(Succeeds)eWasNotifyingeWasNotifying
UndoeWasOpenForUndoeWasOpenForUndo(Succeeds)


The best performance approach is to always open the object for READ, analyze if you will modify it and only after this analysis, upgrade its open operation to WRITE using the upgradeOpen() method. It will switch the object's state from READ to WRITE. To get back to READ use the downgradeOpen() method. These methods are very useful. A simple operation to use these methods would be:

void changeColor(AcDbObjectId id) {
AcDbEntity* pEnt = NULL;
if (acdbOpenObject(pEnt, id, AcDb::kForRead) == Acad::eOk) {
if (pEnt->colorIndex() != 3) {
pEnt->upgradeOpen();
pEnt->setColorIndex(3);
}
else {
acutPrintf(_T("\nEntity already has color=3"));
}
pEnt->close();
}
}


Using Transaction method

Transactions are a better and much more efficient method to manage objects. They can be nested and this allows you to perform long operations without the limitation of READ and WRITE states. Basically you need to open a transaction, perform your desired modifications, and at the end, perform an end or abort transaction operation.

Transactions are pretty good to use when your application uses dialog boxes that change objects. At your dialog opening you start a new transaction and, depending on user click on OK or CANCEL button, you call end or abort transaction methods.

When you abort a transaction the whole contained modification are cancelled. In fact, modifications are really applied only when you end a transaction. Another great feature is that you can open an object for WRITE several times as you can open for READ at the same time.

You don't need to close objects opened through a transaction. The end or abort method will close every object opened and will perform modifications as necessary. It is not recommended to mix standard OPEN / CLOSE approach with TRANSACTIONS due some design limitations. You could read more details about this inside SDK documentation. Now, let's the see the same operation we did above using transactions:

void changeColor(AcDbObjectId id) {
AcDbEntity* pEnt = NULL;
acdbTransactionManager->startTransaction();
if (
acdbTransactionManager->getObject((AcDbObject*&)pEnt, id, AcDb::kForRead) == Acad::eOk) {
if (pEnt->colorIndex() != 3) {
pEnt->upgradeOpen();
pEnt->setColorIndex(3);
}
else {
acutPrintf(_T("\nEntity already has color=3"));
}
}
acdbTransactionManager->endTransaction();
}

This time you open the object using the getObject() method which is much like acdbOpenObject() but YOU DON'T NEED TO CLOSE the object. The whole process is ended at endTransaction() method call. At that time all operations are applied in one operation.

27 comments :

Anonymous said...

Both Meyers & Sutter strongly warn against using C-style casts.

There are (at least) three ways to get rid of it: (pasting source code with < and > doesn't work)

* use reinterpret_cast:

* use a temporary AcDbObject* variable

* use a template utility function

Unfortunately, AcDbTransactionManager::getObject() isn't template-ized (because it's also "virtual").

Fernando Malard said...

Hello Daniel,

Thank you to contribute once more to this course.

On this particular example I did not perform any error check or safe casting for sake of code simplicity.

By the way, the best way to do a safe cast inside ObjectARX is using the proper cast() method implemented natively by every AcDb native class and every custom class.

So, in this code the best way to do would be open the object with an AcDbObject pointer and then try to cast it to an AcDbEntity pointer. This is the best way to go:

AcDbObject* pObj = NULL;
acdbTransactionManager->getObject(pObj,id,AcDb::kForRead);
AcDbEntity* pEnt = AcDbEntity::cast(pObj);
if (pEnt == NULL) return;
etc.

This way we can assert that the cast is safe.

Be careful when using standard C++ and MFC rules inside ObjectARX because there are a lot of limitations regarding to memory allocation, casting, RTTI, heap management and so on.

Regards.

Anonymous said...

I am very happy to see this web site
can you any one help me to create custom object using objectarx

please give me a complete simple example of creating customobject.

if not possible please let me know how to get this info from whom

what is the procedures

my email ID is
esu_das@rediffmail.com
my name
ESUDAS D

Fernando Malard said...

Hi ESUDAS,

I will cover custom objects soon on this course but I need to first finish some basics to get there.

Stay tuned for the next classes.
Regards.

Anonymous said...

Hi Fernando,

Please can u tell me how to prevent user from unload ARX file when it loaded.

and how to make ARX file to loaded automatically when launched AutoCAD

Sarah,
Cheers

Fernando Malard said...

Hi Sarah,

First I would like to excuse about your post because I have to use the approval system to avoid SPAM at my blog. Due that the post may take some time to appear online.

Regarding to your questions:

1) There is a couple of functions which makes your application locked or unlocked. It needs to be done from inside your kInitAppMsg callback method like this:


acrxDynamicLinker->lockApplication(pkt);

acrxDynamicLinker->unlockApplication(pkt);


pkt is the void* passed to your method.

2)To load your application automatically there are several ways to do that. You will need to get more info inside ObjectARX documentation (take a look at "Loading an ObjectARX application" section inside ObjectARX Developers Guid). In summary, you may:

-Demand load through windows registry entries;
-Use the acad.lsp/.fas file;
-Put your modules inside Startup suite (APPLOAD command);
-Use acad.rx file.

Hope this help.
Regards,

Anonymous said...

Hi Fernando,

I Very sorry, I think you reject My post.

Anyway you are great, it works.

Thanks Fernando.

Regards,
your small sister Sarah.

Anonymous said...

Hi Fernando,
I was wondering whether I can roll back creation of a new dictionary in named object dictionary. I have following two steps to perform as a unit.

1. Create a new dictionary in named object dictionary.
2. create a new XRecord and append it to the newly created dictionary.

Incase the creation of new XRecord fails, I wan't to abort creation of new dictionary. Can I use the Transaction Manager to achieve this. I plan to use the following code.

AcDbDictionary *pNamedObj, *pDict;
AcTransaction *pTrans = actrTransactionManager->startTransaction();

//create new dictionary.
pNamedObj->setAt("JPKSPKXREC_DICT", pDict, DictId);
pNamedObj->close()

//create new XRecord and abort if creation failed.

AcDbXrecord *pXrec = new AcDbXrecord;
if (pDict->setAt("JPKSPKXREC", pXrec, xrecObjId) != Acad::eOk) {
pDict->close();
actrTransactionManager->abortTransaction();
}
else {
pDict->close();
actrTransactionManager->endTransaction();
}

In case I can use the above code, do I have to explicitly close the pNamedObj and pDict or the Transaction Manager will manage this.

regards,
Jatin.

Fernando Malard said...

Hi Jatin,

It should work but you will need to get the Dictionary pointers using getObject() method through the acdbTransactionManager() pointer.

Further, when the object pointer is acquired through a transaction it does not need to be closed. The transaction mechanism will take care of this pointer.

Regards.

Anonymous said...

How do I add a new dictionary record in named object dictionary using the acdbTransactionManager? I looked for AcDbTransactionManager class in objectarx documentation but could not find relevant information.

Fernando Malard said...

Jatin,

I have not tried this before but there is a procedure for newly created objects that you need to participate into a transaction. It is called addNewlyCreatedDBRObject().

A dictionary entry is an AcDbDictionary object so try to add this new object pointer to the transaction and see what happends.

Regards,
Fernando.

Anonymous said...

Hello Fernando.

I draw a simple square and selected it. I was wondering is it possible to get it's properties and write it down in some external file?(e.g. type: polyline, layer: ..., Color:... Closed:...)

regards

Fernando Malard said...

Hello,

Sure, just open the entity for read and start calling its methods to retrieve the desired properties.

Some will be available at AcDbEntity's level, some at the specific class level. In this case, to get some specific information you will need to downcast the pointer from AcDbEntity down to its specific class like AcDbCurve, AcDbPolyline, etc.

To generate the output file, use the CFile class which is pretty easy to use.

Regards.

Anonymous said...

Sorry but I don't understand "open for read". I'm preety much new in this:)
thanks

Fernando Malard said...

Hello,

I meant object management (which is explained on this page).

I would recommend you to play around with this Blog's classes before trying to code anything.

Regards.

Anonymous said...

Hello Fernando.

How deep should I go to get those properties? Am I on the roght way? At the moment I'm at pEnt->desc()->desc()->name().

thanks

Fernando Malard said...

Hi,

With the pEnt pointer you can get the color, layer, linetype, etc.

If the entity is a LINE, try to downcast the pointer with:

AcDbLine* pLine = AcDbLine::cast(pEnt);
if (pLine)
{
AcGePoint3d startPt = pLine->startPoint();
AcGePoint3d endPt = pLine->endPoint();
...
}

AcDbCircle* pCirc = AcDbCircle::cast(pEnt);
if (pCirc)
{
double dRad = pCirc->radius();
...
}

So this really depend on what you want to do with the selected entity and what type of entity are you expecting.

Good luck.

Anonymous said...

Hi. One more question: what type of variables are those parameters? Could I represent for example type POLYLINE as string?

regards

Fernando Malard said...

Hi,

There are several type of parameters. Most of them are ObjectARX specific. For example, AcGePoint3d class represent the 3 coordinates X,Y and Z and each one is a double type.

Check the ObjectARX documentation for further information about types and their corresponding C++ native types.

Regards.

Anonymous said...

Hi Fernando.

Is there any other source of information regarding ObjectARX types and their corresponding C++ native types? I have some problems with Help. It says:

"The ObjectARX Wizards does not anymore links to ObjectARX help files because we now provide a much better mechanism to use ObjectARX Help files while developing ObjectDBX/ARX applications"

thanks

Fernando Malard said...

Hi,

Open the ObjectARX help file at:

\ObjectARX 2009\docs\arxref.chm

Regards.

Anonymous said...

Hello Fernando.

I have troubles with this entity properties. I want to read them from my .dwg file and write it down in simple .txt file. Is there any example how to do that? I get color, start and end points (for example) but I have problems with ACHAR type(linetype, layer...).

thanks in advance

Fernando Malard said...

Hi,

ACHAR represent a string.
For example:

ACHAR* layer() const;

You can use CString to retrieve the value as follows:

// Suppose you have your entity's pointer as pEnt

CString sTemp;
sTemp.Format(_T("%s"),pEnt->layer());

Pay attention with some methods that require you to deallocate the returned string. Always read the ObjectARX docs to be aware of this.

That's it.

Unknown said...
This comment has been removed by the author.
Fernando Malard said...

Hi Sandhya,

You can use acedSetVar() and acedGetVar() to set and retrieve AutoCAD variables.
I would recommend you to first store the current OSMODE value to restore it after your command is finished so you don't mess with user's current configuration.

Regards,

Anonymous said...

If there is one assembly as .dwg file having different components but i want to edit each component separately like i want to retrieve name,size etc of that component. So, how i can select each component through coding? And how can differentiate between the type of component?

Fernando Malard said...

Hello,

First I need to understand what do you mean by the term "components". Are you talking about Blocks?
If you are talking about blocks, they are structured inside each dwg as follows:

- Each block definition (AcDbBlockTableRecord) is stored into AcDbBlockTable container through a unique name used as its key

- Blocks can be inserted into blocks by using AcDbBlockReference entities so, for example, inside Model Space (which is a BlockTableRecord) you can have several AcDbBlockReference entities pointing to their corresponding AcDbBlockTableRecord entries inside AcDbBlockTable

- Each block insertion (AcDbBlockReference) will draw the block using local context (position, rotation, scale)

- To collect all block insertions into a dwg you would need to first know the block name you are looking for. Then, with this name, you can go to the AcDbBlockTable of this dwg and open this entry (using the name you know). With this pointer (AcDbBlockTableRecord*) you can then use the following method to retrieve all "block insertions" of this block:



Acad::ErrorStatus getBlockReferenceIds(
AcDbObjectIdArray& ids,
bool bDirectOnly = true,
bool bForceValidity = false
) const;



The array returned by this method will allow you to open each AcDbBlockReference and collect further information about each block insertion.

Hope this helps.

Regards,