As discussed in the previous post, AutoCAD is now largely an MDI application, and this can have an impact on the design of applications.
Let’s talk about the theoretical issue with migrating applications into a multiple-document environment. We used to highlight this nicely, back when we first started talking about MDI in AutoCAD 2000. We demonstrated a simple ObjectARX application that defined an alternative RECTANGLE command. The code used in the demo can still be found in the ObjectARX SDK, under samples\editor\rect. The original command - as defined in the badrectang.cpp file – makes use of static member variables to store information globally about the width of the rectangle's line segments, its fillet radius or chamfer (if applicable), etc.
There were three versions of the application that we showed: the first was a standard SDI application, the second was the same code simply declaring itself to be MDI-aware, and the third was a fully ported, MDI-aware application.
The SDI Version
When loaded, the first version of the application forces AutoCAD into SDI mode (the equivalent of the SDI variable being set to 1). That’s because modules need to declare themselves as MDI-aware by calling AcRxDynamicLinker::registerAppMDIAware() or acrxRegisterAppMDIAware() during their AcRx::kInitAppMsg handler (an initialization message received in a module’s acrxEntryPoint). Incidentally, if you’re using the ObjectARX Wizard these days you probably won’t see this call, as it’s hidden down in the guts of AcRxDbxApp::On_kInitAppMsg().
Here’s what you get when you try to load an SDI module into AutoCAD:
ARX application : C:\Program Files\Autodesk\ObjectARX 2007\samples\editor\rect\Rectang.arx is NOT MDI aware.
For a number of releases there was even a frowny face - :-( - in this message, but thankfully that was eventually removed (and none too soon).
This first version of the module forces AutoCAD into SDI mode – and AutoCAD takes a lowest common denominator approach in this regard – you would then have to unload the module to allow AutoCAD to return to MDI mode. Loading an SDI-only module changes the value of the SDI variable to be 2 (or 3, if the variable was previously set to 1). Being forced into SDI mode means that when a new drawing is created or loaded into the editor, AutoCAD needs to close the existing, open document. It also means there has to be exactly one document open in the editor at all times – you cannot close it to go to zero-doc mode.
Having this compatibility mode for applications that were not MDI-aware saved developers from having to invest immediately in an MDI port, but be warned – the variable is documented as no longer being supported, and it will go away at some point:
“Note In future releases of AutoCAD, the SDI system variable will be removed. At present, SDI is available but it is not supported.
Some commands and features are not available when you operate in single document interface mode.”
So moving on…
The Falsely MDI-Aware Version
The next version of the application adds a call to acrxRegisterAppMDIAware(), and was really the fun one to demo. The code still makes use of static variables for all its settings, which raises the potential for very strange application behaviour. Let’s take this example:
Start drawing a rectangle in one drawing. You select one corner, and AutoCAD prompts for the second.
Rather than selecting it, you switch to another drawing loaded into the editor. This, in itself, is not a problem, but in this drawing you can launch the rectangle command again – while it is still active in the first drawing – and change the values of global variables that all instances of the command depend upon.
So you can add a thickness to the second rectangle, which changes the global variable, and create some strange effects in the first rectangle – depending on the application logic for drawing it. Switching back, here’s an example of the funny behaviour you see: the fillet has been partially applied and the corner has jumped… all very peculiar.
The Properly MDI-Aware Version
This is the fully ported version of the command, and behaves just like the RECTANG command in AutoCAD.
The trick is to maintain a set of variables representing the command’s settings (chamfer distance, fillet radius, segment width, etc.) for each open document.
The documentation regarding MDI-enabling your application talks about encapsulating your data into a class (fair enough) but then goes on to describe how to create a linked list of these objects, one per document (ugh). At the time I remember finding this inelegant, so got to work on a utility class that made use of a more modern – and better suited – data structure, a map (which ultimately uses a hash table).
At the time I hadn’t spent a great deal of time working with templates or maps, so my first attempt – although quite functional – was a mess. I had used an MFC container class to implement it, and it stretched over a number of header and implementation files. All proud, I circulated it to my colleagues around the world, and within a few hours received back a beautifully crafted version based on STL that took all of about 10 lines. That version – or a slightly tweaked version of it – is still in the SDK, in the AcApDMgr.h file. Here’s the relevant code from that header:
#include "acdocman.h"
template <class T> class AcApDataManager : public AcApDocManagerReactor {
public:
AcApDataManager () {
acDocManager->addReactor (this) ;
}
~AcApDataManager () {
if ( acDocManager != NULL )
acDocManager->removeReactor (this) ;
}
virtual void documentToBeDestroyed (AcApDocument *pDoc) {
m_dataMap.erase (pDoc) ;
}
T &docData (AcApDocument *pDoc) {
std::map<AcApDocument *, T>::iterator i =m_dataMap.find (pDoc) ;
if ( i == m_dataMap.end () )
return (m_dataMap [pDoc]) ;
return ((*i).second) ;
}
T &docData () {
return (docData (acDocManager->curDocument ())) ;
}
private:
std::map<AcApDocument *, T> m_dataMap ;
} ;
This helpful little template class allows you very easily to add per-document data to your application.
The first step we showed in the demo was the encapsulation of the data. The “bad” version of the app makes this easy for us, as it has the variables already encapsulated:
class CRectInfo {
public:
CRectInfo();
// First point selection.
AcGePoint3d m_topLeftCorner;
// First Chamfer distance.
static double m_first;
// Second Chamfer distance.
static double m_second;
// Bulge value.
static double m_bulge;
// Elevation.
static double m_elev;
// Thickness.
static double m_thick;
// Width.
static double m_width;
// Fillet radius.
static double m_radius;
// Filleting or chamfering.
bool m_cornerTreatment;
bool m_elevHandSet;
// Vector of chamfer.
AcGeVector3d m_chamfDirUnitVec;
};
// Definition in file scope.
//
double CRectInfo::m_first;
double CRectInfo::m_second;
double CRectInfo::m_bulge;
double CRectInfo::m_elev;
double CRectInfo::m_thick;
double CRectInfo::m_width;
double CRectInfo::m_radius;
static CRectInfo plineInfo;
This gets changed to:
class CRectInfo {
public:
CRectInfo();
// First point selection.
AcGePoint3d m_topLeftCorner;
// First Chamfer distance.
double m_first;
// Second Chamfer distance.
double m_second;
// Bulge value.
double m_bulge;
// Elevation.
double m_elev;
// Thickness.
double m_thick;
// Width.
double m_width;
// Fillet radius.
double m_radius;
// Filleting or chamfering.
bool m_cornerTreatment;
bool m_elevHandSet;
// Vector of chamfer.
AcGeVector3d m_chamfDirUnitVec;
};
AcApDataManager<CRectInfo> rectDataMgr; // MDI Safe
An important note is that the class needs to initialize its members properly in its constructor. The reason for this is mainly around the way the map works: when you first query for the custom data related to a particular document, if the document is not found as a key in the map, a new instance of the custom data class is created and returned, right there. So you need to make sure its default settings make sense.
Otherwise, there's not that much more to explain. All references of the old class' members, such as plineInfo.m_first - now need to make use of the AcApDataManagar class instance to access the data. To make life really, really easy, we used a #define to save the search & replace:
#define plineInfo rectDataMgr.docData()
This means plineInfo.m_first will automatically be expanded by the pre-processor to rectDataMgr.docData().m_first. The docData() call gets the right data container object for the current document. If you want the data for another document, just pass the AcApDocument pointer into docData().
One finaly point to mention: the SDK sample was added prior to the AcApDocManager class being officially added to the inc folder, so it uses its own version of the class, called AsdkDataManager defined in a local DataMgr.h file. You may also come across Wizard-generated projects creating local instances of this file (recent versions just use the AcApDMgr.h), along with DocData.h/.cpp files, ready to be populated with your application's per-document data.
Next up is how to handle per-document data in .NET. And don't worry - that'll be a much shorter topic... :-)