COM Wrapper——A tutorial on A COM server for Custom Object

COM Wrapper

——A tutorial on A COM server for Custom Object

Version 1.0

September 13th 1999

By Chandar Kumar Oddiraju

Autodesk Developer Consulting Group

Table of contents

1	INTRODUCTION	2
1.1	CREATE A BLANK PROJECT WORKSPACE	3
2	CREATING THE CUSTOM OBJECT MODULE(DBX)	4
2.1	CREATE A PROJECT TEMPLATE	4
2.2	ADDING CUSTOM OBJECT	5
2.3 ADD CLASS	5
2.4	DEFINE CUSTOM OBJECT ATTRIBUTES	6
2.5	FUNCTIONS YOU OVERRIDE	6
2.6	MODIFY THE MEMBER FUNCTIONS	7
2.7	IMPLEMENTATION OF GETCLASSID IN CUSTOM OBJECT	7
2.8	BUILD ASDKRINGS.DBX	7
3	CREATING THE WRAPPER CLASS FOR THE CUSTOM OBJECT	8
3.1	CREATE A PROJECT TEMPLATE	8
3.2	OBJECTARX APPWIZARD CREATED SOURCE CODE	9
3.3	ADDING A NEW CUSTOM OBJECT WRAPPER	10
3.4	IMPLEMENTATION OF GETCLASSID IN CUSTOM OBJECT REVISITED	13
3.5	ADDING A NEW PROPERTIES	14
3.6	OBJECTARX APPWIZARD GENERATED FUNCTIONS.	15
3.7	MODIFICATIONS YOU NEED TO DO	17
3.8	FINAL CODE:	18
3.9	ADDITIONAL PROJECT SETTINGS FOR WRAPPER APPLICATION.	21
3.10	BUILD THE COM WRAPPER APPLICATION	23
4	TESTING THE COM WRAPPER	24
5	ADDING OPM FEATURE.	26
5.1	ATL OBJECT WIZARD PROPERTIES - OPM TAB SETTINGS	26
5.2	DEVELOPMENT OF OPM PROPERTIES.	26
5.3	MODIFY THE OPM OVERRIDE FUNCTIONS	29

1 Introduction

In this tutorial you will see how to make your ObjectARX custom objects available to COM. Doing so brings two primary benefits:

  1. You can leverage the ease of use of languages such as Visual Basic, Visual Basic for Applications, Java and Delphi to manipulate your custom objects.

  2. The Properties Window feature of AutoCAD will be able to interact with your object intelligently.

Creating a 'COM wrapper for your custom object requires a reasonable amount of work. Autodesk provides two tools to make this task easier: an ATL-based framework that implements most of the interfaces that you need on a COM wrapper and the ObjectARX Wizards that let you configure this framework through easy to use dialogs. For more information on the ATL based framework, see Chapter 23 in ObjectARX Developers Guide "COM, ActiveX Automation, and the Object Property Manager " or consult the online documentation.

To create a COM wrapper you need to have an ObjectARX custom object. In the first part of the tutorial, we will build a simple custom object, then in the second half of the tutorial to create a COM wrapper for our custom object.

1.1 Create a blank project workspace

  1. On the File menu, click New.
  2. Click the Workspaces tab.
  3. Select Blank Workspace from the type list, and type a “Labs” in the Workspace Name box.
  4. Click OK

Now your Visual studio has only a blank workspace without any project in it.

2 Creating the custom object module(DBX)

Let’s create a project that defines custom entity. Use the ObjectARX Application Wizard to create this project. Make sure that you download the latest version of the ObjectARX Wizard from the ADN web site. The version that comes with ObjectARX CD (or if you downloaded the ObjectARX SDK) does not contain the latest version of the wizard. To carry out this tutorial, you need latest ObjectARX wizard, you can download the latest version of the wizard from

http://partnersys.autodesk.com/adn_cd/doc/adn_cd/adn/techsupp/devcon/acad2000/default.htm

2.1 Create a project template

Start the wizard from the ‘File->New…’ menu entry. In the ‘New’ dialog box select the ‘Projects’ tab (if it’s not already selected) and select the ‘ObjectARX 2000 AppWizard’ from the wizard list. Enter ‘MyRings’ for the project name and click on the ‘OK’ button. Be sure that you check the ‘Add to current workspace’.

The next dialog you are presented with, is the ObjectARX 2000 AppWizard. First, you have to enter your registered developer symbol. Use ‘Asdk’ for this sample. To register you own developer symbol please go to

http://www.autodesk.com/solution/partners/adn/symbols/index.htm.

Make sure you have the ‘ObjectDBX (Custom object definition)’ option checked. Press the ‘Finish’ button and press ‘OK’ on the next dialog. Now you have a project that contains the skeleton for an ObjectDBX application

2.2 Adding Custom object

Let’s define a custom entity and add it to our project.

Click the “ObjectARX class Wizard” button on ObjectARX tool bar.

2.3 Add class

You will be presented with the ObjectARX class Wizard dialog. Click the ‘Add Class’ button on the dialog to add a new class.
In Class name enter “Rings” and in DXF name enter “RINGS”. Then click OK.

2.4 Define Custom Object Attributes

Next, we will add our Custom Object Attributes. Click the Member Variables tab, then click the Add Variable button.

Similarly, enter the following variables one at a time.

Name Type DXF
m_circle AcGePoint3d 10
m_normal AcGeVector3d 11
m_radius Double 140
m_rings Adsk::Uint8 280

2.5 Functions you Override

Click Member Function tab on the dialog. From the ‘Available member functions’ list box select and add the worldDraw(), dwgInFields(), dwgOutFieds() and getClassID() functions to “AsdkRings” to override.

2.6 Modify the member functions

The wizard sets everything up for you except the code in worldDraw() function. Modify the function as shown below.

Adesk::Boolean AsdkRings::worldDraw(AcGiWorldDraw* mode)
{
	assertReadEnabled();
	// TODO: implement this function.
	double step = m_radius / m_rings; // calculate the increment for each ring
	double radius = step; // initialize the first radius

	// now you draw a circle for each radius.
	for(int i = 0; i < m_rings ; i++, radius += step)
		mode->geometry().circle(m_center, radius, m_normal);

	return AcDbEntity::worldDraw(mode);
}

2.7 Implementation of getClassID in Custom Object

The important function that has to be overridden by the custom object is getClassID. This function is used by AutoCAD whenever AutoCAD needs to retrieve the COM wrapper for a custom object given a pointer or id of the custom object. This happens for example when the user selects the custom object while the AutoCAD Properties Window is active.
To implement this function correctly you need the CLSID of your COM wrapper, something that we do not have at this point. We will come back to this function in Section 3.4.

2.8 Build AsdkRings.dbx

Now you are ready to build the project. After a successful build, copy the AsdkRings.dbx file into “C:\Program Files\Common Files\Autodesk Shared\” directory. If you want, you can use a different directory however you are advised to put the directory in System Path. Otherwise, you are required to load the .DBX file manually into AutoCAD. Your COM server implementing the COM wrapper will have a load-time dependency on your custom object. The operating system must be able to resolve this dependency at load time and the best way to make sure that it is possible is to put your DBX on the operating system path. For more information on how the OS resolves DLL, dependencies see DLL topics on MSDN.

3 Creating the wrapper class for the custom object

Now you will provide COM support for our Rings Object.

3.1 Create a project template

Start the wizard from the ‘File->New…’ menu entry. In the ‘New’ dialog box select the ‘Projects’ tab (in case it’s not already selected) and select the ‘ObjectARX 2000 AppWizard’ from the wizard list. Enter ‘RingsWrapper’ for the project name and click on the ‘OK’ button. Be sure that you check the ‘Add to current workspace’
Click OK. The next dialog you are presented with, is the ObjectARX 2000 AppWizard. First, you have to enter your registered developer symbol as before.
Use ‘Asdk’ for this sample. To register you own developer symbol please go to

http://www.autodesk.com/solution/partners/adn/symbols/index.htm.

Check the ‘Com Server’ check box. This will activates the ‘Use ATL’ and ‘Use ATL Extensions for Custom Objects’ check boxes. Check these two check boxes. However as you are writing the COM wrapper around the custom objects, they will be checked by default.

Select the ‘Finish’ button and click ‘OK’ on the next dialog.

3.2 ObjectARX AppWizard created source code

Now you have a project that contains the skeleton for an ObjectARX application which uses ATL. The project contains a number of files. Open the RingsWrapper.cpp file you can see that it implements the necessary functions needed for ObjectARX, COM and ATL:

CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_Rings, CRings)
END_OBJECT_MAP()

bool isModuleLoaded(const char* str)
{
	AcDbVoidPtrArray* pApps = reinterpret_cast<AcDbVoidPtrArray*>(acrxLoadedApps());
	if(pApps==NULL)
		return false;
	bool bFound = false;
	for(int i=0;i<pApps->length();i++)
	{
		if(stricmp(reinterpret_cast<const char*>(pApps->at(i)),str)==0)
		{
			bFound = true;
			break;
		}
	}
	for(;pApps->length()>0;)
	{
		delete reinterpret_cast<char*>(pApps->at(0));
		pApps->removeAt(0);
	}
	delete pApps;
	return bFound;
}

/////////////////////////////////////////////////////////////////////////////
// DLL Entry Point
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{


	if(dwReason == DLL_PROCESS_ATTACH)
	{
        _hdllInstance = hInstance;
		_Module.Init(ObjectMap, hInstance, &LIBID_AsdkRINGSWRAPPERLib);
		DisableThreadLibraryCalls(hInstance);

	} else if(dwReason == DLL_PROCESS_DETACH) {

		_Module.Term();
	}
	return TRUE;    // ok
}
/////////////////////////////////////////////////////////////////////////////
// Used to determine whether the DLL can be unloaded by OLE
STDAPI DllCanUnloadNow(void)
{
	return(_Module.GetLockCount()==0) ? S_OK : S_FALSE;
}

/////////////////////////////////////////////////////////////////////////////
// Returns a class factory to create an object of the requested type
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
	return _Module.GetClassObject(rclsid, riid, ppv);
}

/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
	// Registers object, typelib and all interfaces in typelib
	return _Module.RegisterServer(TRUE);
}

/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
	return _Module.UnregisterServer(TRUE);
}

This file defines an instance of the CComModule class and an (currently) empty object map for this instance. Then you have the DllMain() function which initializes and terminates the CComModule instance.

Windows calls the function DllCanUnloadNow() to test if it can unload your application, and DllGetClassObject() to create an instance of one of the COM objects your application defines. The DllRegisterServer() function registers your COM server application in the Windows registry, and the DllUnregisterServer() function removes the entries from the Windows registry if your application should be removed.

The other functions created by the wizard are standard ObjectARX functions. You have the acrxEntryPoint() function which is called by AutoCAD when your application is loaded. The AddCommand() function which used for adding new commands to your project. But since this ARX is not the usual ObjectARX application, it is recommended NOT to register any commands in it. In InitApplication() you call DllRegisterServer() function and the UnloadApplication() will remain empty.

There’s one point that must be clear at this time. Although the DLL we have created is a COM server, it can only be loaded into the AutoCAD process. This is because the DLL is an ObjectARX application that has load-time dependency on acad.exe. We provide an acrxEntryPoint() function in this DLL so that the user can load it on the AutoCAD command line using the ARX command. In the acrxEntryPoint() function the DLL calls DllRegisterServer() thus registering itself in the registry. Note that once the DLL is registered COM will be able to load this DLL via its own activation mechanism. It is important to understand that when COM loads the DLL the acrxEntryPoint() function is NOT called. Therefore, it is advisable to do only self-registration when acrxEntryPoint() is called an all other necessary initialization is done in DllMain().

You can compile this project and load the created .ARX file in AutoCAD, but currently it does nothing

3.3 Adding a new Custom object wrapper

Let's insert a new ATL object. From the “Insert” menu of Developer Studio, select “New ATL Object”. This button is also there on ObjectARX tool bar

You will be presented with ATL Object Wizard Dialog. Select ObjectARX 2000 in the Category
list and select Com Object Wrapper in the Objects list.

Press the ‘Next>’ button, you will see the ATL Object Wizard Properties dialog. Here you can define the new COM object. Click the ‘Names’ tab. In the ‘Short Name:’ field, enter ‘Rings’. The ATL Object Wizard fills the other fields automatically:

Select the “Automation to Database Connection” tab on the wizard properties dialog.

Enter “AsdkMyRings” in the Name of the AcDb Server field, because AsdkMyRings.dbx is the application that implements the ObjectARX custom object.
Enter “AsdkRings” in the Name field in the AcDbClass to Automate section. This is the name of the class you defined in asdkRings.h file and is used in the AsdkMyRings application.
Select the header file that has the declaration of our custom object. Use Browse button to select the file AsdkRings.h in CustObj project directory.
Select “AcDbEntity” in the Derived From combo box.
Click Ok to close the dialog.

Here I have deliberately not discussed the OPM tab. It is discussed in Section 5. If you are interested in adding OPM to your application, follow the steps outlined in section 5.1 and return here to resume. Afterwards I will remind you what you need to do for this extra feature as and when required. In this section, we only focus on wrapping the custom object.

The AppWizard modifies the code in DllMain() function. Now the code looks like this:

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
    
    #pragma comment(lib, "AsdkMyRings.lib")
    static const char* strAsdkMyRings = "AsdkMyRings.dbx";

	if(dwReason == DLL_PROCESS_ATTACH)
	{
        _hdllInstance = hInstance;
		_Module.Init(ObjectMap, hInstance, &LIBID_AsdkRINGSWRAPPERLib);
		DisableThreadLibraryCalls(hInstance);
        // check if the ARX app is loaded or not.
        // if not, load it as UI so that you won't have
        // proxy if this DLL is unloaded by OS
        if(!isModuleLoaded(strAsdkMyRings))
        {
            if(!acrxLoadModule(strAsdkMyRings, false, true))
 	           return FALSE; //this will trigger a DLL_PROCESS_DETACH right away
        }
        //bump the reference count 
        acrxLoadModule(strAsdkMyRings, false, false);

	} else if(dwReason == DLL_PROCESS_DETACH) {
        //try to decrease the ref count on the DBX 
        //if you couldn't load it then this a no op.
        acrxUnloadModule(strAsdkMyRings);

		_Module.Term();
	}
	return TRUE;    // ok
}

This is added by the wizard, since you are wrapping around the Rings custom object it is good idea to check whether the custom object application is loaded or not.

Note that the COM server DLL has a load-time dependency on the DBX application implementing the custom object so the Operating System loads the DBX application into memory when the COM server DLL is loaded.

Why do we need to check if the DBX is loaded? Here’s why:
Since the DBX is loaded into memory by the OS implicitly due to a DLL dependency, AutoCAD does not know about this DBX. The isModuleLoaded() check will return false in this case and we will call acrxLoadModule() to tell AutoCAD to load the DBX. Of course, AutoCAD won’t load the DBX into memory again, rather it will just do the usual initialization that happens each time a DBX/ARX application is loaded i.e. the acrxEntryPoint() of the DBX is called.

Note, that we call acrxLoadModule() twice in this case. First as the ‘user’ and then as a client application. AutoCAD implements a reference-counting scheme to decide when to unload an ARX/DBX application. The COM server can be loaded/unloaded several times during a session of AutoCAD as COM wrappers are created and destroyed. However, we don’t want the underlying database objects to be turned into proxies and resurrected them each time when this happens. We want the user to be in ultimate control of when the DBX is unloaded. The reference count on the user’s behalf will ensure this.

Apart from this modification, AppWizard also generates Rings.cpp and Rings.h files, which has the CRing class implementation and definition respectively. Read on for further discussion of the contents of these files.

If you included the OPM feature into your project, read topic 5.2 and then return to here.

3.4 Implementation of getClassID in Custom Object Revisited

Now we have everything we need to properly implement the getClassID() function that we stubbed out in Section 2.7 for 'AsdkRings'.
Modify the implementation in asdkRings.cpp as follows:

Acad::ErrorStatus AsdkRings::getClassID(CLSID* pClsid) const
{
	assertReadEnabled();
	// TODO: implement this function.
	*pClsid = CLSID_Rings;
	return Acad::eOk;
}

The CLSID_Rings is defined in RingsWrapper_i.c file. This file is generated from the RingsWrapper.idl file when you build the RingsWrapper project.
Copy the definition CLSID_Rings from RingsWrapper_i.c to just above the getClassID() function in asdkRings.cpp. Alternatively, you could include the RingsWrapper_i.c into your Rings project but it easier to copy this value since it is not going to change anyway.

3.5 Adding a new properties

Expand the class view in the workspace and expand the RingsWrapper project and right click on IRings Interface to get the popup context menu. Select Add Property.
You will be presented with the Add Property to Interface dialog. Enter “rings” in Property Name field, select short Property type from the combobox. Ensure Get Function and Put functions are checked in Function type group box. Also select PropPut radio button.(These are the defaults)

Click OK to add this property into Rings object..

Repeat the same process for adding the following

Property Name Property Type
Rings Short
Center VARIENT
Radius Double
Normal VARIENT

3.6 ObjectARX AppWizard generated functions.

First let us look into the RingsWrapper.idl file you will see the following functions in IRings Interface section.

interface IRings : IAcadEntity
{
	[propget, id(1), helpstring("property rings")] HRESULT rings([out, retval] short *pVal);
	[propput, id(1), helpstring("property rings")] HRESULT rings([in] short newVal);
	[propget, id(2), helpstring("property center")] HRESULT center([out, retval] VARIANT *pVal);
	[propput, id(2), helpstring("property center")] HRESULT center([in] VARIANT newVal);
	[propget, id(3), helpstring("property radius")] HRESULT radius([out, retval] double *pVal);
	[propput, id(3), helpstring("property radius")] HRESULT radius([in] double newVal);
	[propget, id(4), helpstring("property normal")] HRESULT normal([out, retval] VARIANT *pVal);
	[propput, id(4), helpstring("property normal")] HRESULT normal([in] VARIANT newVal);
};

Here every property is assigned with unique id value, each property has been assigned with two distinct function types one of, which is ‘propput’ for setting the value, and other is ‘propget’ for retrieving the value. AppWizard will generate functions based on these function types. For 'propput' you will have put_XXX() functions and for 'propget' you will have get_XXX() function, where XXX is the property name. You can change the helpstring according to your requirements.

Now look at the Rings.cpp and Rings.h files that have the actual implementation our Custom Object wrapper class.

Rings.h File:

#include "resource.h"       // main symbols
#include "..\MyRings\AsdkRings.h"
#include "axtempl.h"

We are including the actual AsdkRings header file to get the definition of our Custom Object AsdkRings

/////////////////////////////////////////////////////////////////////////////
// CRings
class ATL_NO_VTABLE CRings : 
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CRings, &CLSID_Rings>,
	public ISupportErrorInfo,
    public IAcadEntityDispatchImpl<CRings,&CLSID_Rings, IRings, &IID_IRings, &LIBID_AsdkRINGSWRAPPERLib>

The CRings class is a multiple inherited class from CComObjectRootEx, CComCoClass, ISupportErrorInfo and IAcadEntityDispatchImpl. CComObjectRootEx and CComCoClass are provided by ATL. Please see the ATL documentation for details. ISupportErrorInfo is a standard COM interface that has only one method: InterfaceSupportsErrorInfo(). This method is implemented in Rings.cpp. IAcadEntityDispatchImpl represents the ATL-based framework provided on the ObjectARX SDK. It implements a number of interfaces that are required from a Com wrapper. This class is a template class, thus the full source code of this class is available to you on the ObjectARX SDK (see axboiler.h).

{
public:
	CRings()
	{
	}

//this function replaces the DECLARE_REGISTRY_RESOURCEID(IDR_RINGS) macro
static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
{
extern HRESULT setupRegistrar(IRegistrar** p);
    CComPtr<IRegistrar> pReg;
    HRESULT hRes;
    if(FAILED(hRes =setupRegistrar(&pReg)))
        return hRes;
    //In the AutoCAD environment it is better to link ATL dynamically
    return AtlModuleUpdateRegistryFromResourceD(&_Module,
			(LPCOLESTR)MAKEINTRESOURCE(IDR_RINGS), bRegister, NULL,pReg);
}

The regular ATL Wizard will add a macro to update the registry. Through this macro it will call AtlModuleUpdateRegistryFromResourceD () function (You can find this code in atl\include\atlbase.h ) and ATL registers everything by converting to short paths to work around bug in NT4's CreateProcess(). ARX's are loaded with long file name but CoCreateInstance() will bring in another instance of the same DLL because LoadLibrary() is unable to figure out that the DLLs loaded with long and short filenames are actually the same. You avoid this by using another replaceable parameter MODULE_FIXED instead of MODULE in the rings.rgs file (For further details on the ATL Registry Component, also known as the Registrar, see the ATL documentation.) Since we modified the MODULE with MODULE_FIXED we will need to make sure that the Registrar knows how to replace this replaceable parameter. This is what the setupRegistrar() function does.

DECLARE_NOT_AGGREGATABLE(CRings)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CRings)
	COM_INTERFACE_ENTRY(IRings)
	COM_INTERFACE_ENTRY(IDispatch)
	COM_INTERFACE_ENTRY(ISupportErrorInfo)
	COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(IAcadBaseObject)
	COM_INTERFACE_ENTRY(IAcadObject)
	COM_INTERFACE_ENTRY(IAcadEntity)
    COM_INTERFACE_ENTRY(IRetrieveApplication)
END_COM_MAP()


// ISupportsErrorInfo
	STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
// IAcadBaseObjectImpl
    virtual HRESULT CreateNewObject(AcDbObjectId& objId, AcDbObjectId& ownerId, TCHAR* keyName);
    virtual HINSTANCE GetResourceInstance()
    {
        return _Module.GetResourceInstance();
    }
// IRings
public:
	STDMETHOD(get_normal)(/*[out, retval]*/ VARIANT *pVal);
	STDMETHOD(put_normal)(/*[in]*/ VARIANT newVal);
	STDMETHOD(get_radius)(/*[out, retval]*/ double *pVal);
	STDMETHOD(put_radius)(/*[in]*/ double newVal);
	STDMETHOD(get_center)(/*[out, retval]*/ VARIANT *pVal);
	STDMETHOD(put_center)(/*[in]*/ VARIANT newVal);
	STDMETHOD(get_rings)(/*[out, retval]*/ short *pVal);
	STDMETHOD(put_rings)(/*[in]*/ short newVal);
};

Now look at the CreateNewObject() function in Ring.cpp.

HRESULT CRings::CreateNewObject(AcDbObjectId& objId, AcDbObjectId& ownerId, TCHAR* keyName)
{
    try 
    {
        AXEntityDocLock(ownerId);
        Acad::ErrorStatus es;
        AcDbObjectPointer<AsdkRings> pO;
        if((es = pO.create()) != Acad::eOk)
            throw es;
        AcDbDatabase* pDb = ownerId.database();
        pO->setDatabaseDefaults(pDb);
        AcDbBlockTableRecordPointer pBlockTableRecord(ownerId, AcDb::kForWrite);
        if((es = pBlockTableRecord.openStatus()) != Acad::eOk)
            throw es;
        if((es = pBlockTableRecord->appendAcDbEntity(objId, pO.object())) != Acad::eOk)
            throw es;
    }
    catch(const Acad::ErrorStatus)
    {
        //we can become more sophisticated 
        return Error(L"Failed to create AsdkRings",IID_IRings,E_FAIL);
    }
    return S_OK;
}

In this function you create the custom object and append it to its owner in the database. The owner is passed to you as a parameter of this function.
Note that the framework calls this function in response to a call to the IAcadBaseObject::SetObjectId() method.

3.7 Modifications you need to do
All the modifications you do manually are in Rings.cpp file. Let’s see what we need to do in this file.

In case of a single threaded server like AutoCAD, all Automation calls go through the message loop and are dispatched to a hidden window that calls the method. Since, we are driven off the message loop and we are in the application context, we need to explicitly lock documents before doing any operation on the database. You do this by instantiating an AcAxDocLock object by passing our object Id. For details on the AcAxDocLock, class sees the ObjectARX online help.

	AcAxDocLock lock(m_objId);

You also need to check the lock status, if you don’t succeed in locking the document, you are not allowed to do modifications, you must simply return from function.

	if(lock.lockStatus()) // if something wrong return from here..
		return E_ACCESSDENIED;

The next step is to access the custom object that this COM object wraps. The framework stores the object ID of the underlying database object in the m_objId member variable. You can use this member variable anytime to gain access to the custom object. The best way is to use a ‘smart pointer’ to open the object:

	AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForRead); 

In your get methods you open the object for read while in your set methods you open the object for write. Then make sure that open operation succeeds:

	If(pC.openStatus()!=Acad::eOk)
		return E_ACCESSDENIED;

Now, you can delegate to the database object to actually set or get the property.

In get_xxxx(type *pval) functions you call the object method to retrieve the required property and set into pval argument.

While in put_xxx(type newval) functions you call the object method to set the required property to a value given in newval argument.

The task is simple as long as you are dealing with basic types. However, points and vectors are passed as VARIANTS on the COM interface so you will need to convert VARIANTS to AcGePoint3d and AcGeVector3d and vice versa. To deal with AcGePoint3d you use AcAxPoint3d class, which is specially designed for this purpose. AcAxPoint3d is wrapped around AcGePoint3d class to implement for COM specific functionality. See ObjectARX online help for further details.

To convert from VARIANT to AcGePoint3d you do as follows:

STDMETHODIMP CRings::put_center(VARIANT newVal)
{
		. . .
		. . .
		AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForWrite);
		If(pC.openStatus()!=Acad::eOk)
			return E_ACCESSDENIED;
		AcAxPoint3d pt(newVal);
		pC->setCenter(pt);
		. . .
		. . .
}

Since AcAxPoint3d is derived from AcGePoint3d, you can safely pass this object in place of AcGePoint3d object as arguments.

For converting from AcGePoint3d to VARIANT, you call the setVariant() method defined in AcAxPoint3d.

STDMETHODIMP CRings::get_center(VARIANT *pVal)
{
		. . .
		. . .
		AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForRead);
		If(pC.openStatus()!=Acad::eOk)
			return E_ACCESSDENIED;
		AcAxPoint3d pt;
		pC->center(pt);
		pt.setVariant(*pVal);
 		. . .
		. . .
}

Since the AcAxPoint3d constructor, which takes VARIANT type as argument, throws an exception, you must handle them. This is achieved by wrapping the above two statements with a try, catch block.

3.8 Final code:

// Rings.cpp : Implementation of CRings

#include "stdafx.h"
#include "stdarx.h"
#include "RingsWrapper.h"
#include "Rings.h"
#include "axpnt3d.h"

#define AXEntityDocLock(objId)                              \
    AcAxDocLock docLock(objId, AcAxDocLock::kNormal);       \
    if(docLock.lockStatus() != Acad::eOk)                   \
        throw docLock.lockStatus();
/////////////////////////////////////////////////////////////////////////////
// CRings

STDMETHODIMP CRings::InterfaceSupportsErrorInfo(REFIID riid)
{
	static const IID* arr[] = 
	{
		&IID_IRings
        
        ,&IID_IAcadEntity
        
        
        
	};
	for(int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		if(InlineIsEqualGUID(*arr[i],riid))
			return S_OK;
	}
	return S_FALSE;
}
HRESULT CRings::CreateNewObject(AcDbObjectId& objId, AcDbObjectId& ownerId, TCHAR* keyName)
{
    try 
    {
        AXEntityDocLock(ownerId);
        Acad::ErrorStatus es;
        AcDbObjectPointer<AsdkRings> pO;
        if((es = pO.create()) != Acad::eOk)
            throw es;
        AcDbDatabase* pDb = ownerId.database();
        pO->setDatabaseDefaults(pDb);
        AcDbBlockTableRecordPointer pBlockTableRecord(ownerId, AcDb::kForWrite);
        if((es = pBlockTableRecord.openStatus()) != Acad::eOk)
            throw es;
        if((es = pBlockTableRecord->appendAcDbEntity(objId, pO.object())) != Acad::eOk)
            throw es;
    }
    catch(const Acad::ErrorStatus)
    {
        //we can become more sophisticated 
        return Error(L"Failed to create AsdkRings",IID_IRings,E_FAIL);
    }
    return S_OK;
}

STDMETHODIMP CRings::get_rings(short *pVal)
{
	AcAxDocLock lock(m_objId);
	if(lock.lockStatus())
		return E_ACCESSDENIED;
	AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForRead);
	If(pC.openStatus()!=Acad::eOk)
		return E_ACCESSDENIED;

	pC->rings((Adesk::UInt8&)*pVal);
	return S_OK;
}

STDMETHODIMP CRings::put_rings(short newVal)
{
	AcAxDocLock lock(m_objId);
	if(lock.lockStatus())
		return E_ACCESSDENIED;
	AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForWrite);
	If(pC.openStatus()!=Acad::eOk)
		return E_ACCESSDENIED;
	pC->setRings(newVal);
	return S_OK;
}

STDMETHODIMP CRings::get_center(VARIANT *pVal)
{
	try
	{
		AcAxDocLock lock(m_objId);
		if(lock.lockStatus())
			return E_ACCESSDENIED;
		AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForRead);
		If(pC.openStatus()!=Acad::eOk)
			return E_ACCESSDENIED;
		AcAxPoint3d pt;
		pC->center(pt);
		pt.setVariant(*pVal);
	}
	catch(const HRESULT hr)
	{
		Error("An error occurred. Check the input params.");
		return hr;
	}
	return S_OK;
}

STDMETHODIMP CRings::put_center(VARIANT newVal)
{
	try
	{
		AcAxDocLock lock(m_objId);
		if(lock.lockStatus())
			return E_ACCESSDENIED;
		AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForWrite);
		If(pC.openStatus()!=Acad::eOk)
			return E_ACCESSDENIED;
		AcAxPoint3d pt(newVal);
		pC->setCenter(pt);
	}
	catch(const HRESULT hr)
	{
		Error("An error occurred. Check the input params.");
		return hr;
	}
	return S_OK;
}

STDMETHODIMP CRings::get_radius(double *pVal)
{
	AcAxDocLock lock(m_objId);
	if(lock.lockStatus())
		return E_ACCESSDENIED;
	AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForRead);
	If(pC.openStatus()!=Acad::eOk)
		return E_ACCESSDENIED;

	pC->radius(*pVal);
	return S_OK;
}

STDMETHODIMP CRings::put_radius(double newVal)
{
	AcAxDocLock lock(m_objId);
	if(lock.lockStatus())
		return E_ACCESSDENIED;
	AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForWrite);
	If(pC.openStatus()!=Acad::eOk)
		return E_ACCESSDENIED;

	pC->setRadius(newVal);
	return S_OK;
}


STDMETHODIMP CRings::get_normal(VARIANT *pVal)
{
	try
	{
		AcAxDocLock lock(m_objId);
		if(lock.lockStatus())
			return E_ACCESSDENIED;
		AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForRead);
		If(pC.openStatus()!=Acad::eOk)
			return E_ACCESSDENIED;

		AcAxPoint3d pt;
		AcGeVector3d v;
		pC->normal(v);
		pt.set(v.x,v.y,v.z);
		pt.setVariant(*pVal);
	}
	catch(const HRESULT hr)
	{
		Error("An error occurred. Check the input params.");
		return hr;
	}
	return S_OK;
}

STDMETHODIMP CRings::put_normal(VARIANT newVal)
{
	try
	{
		AcAxDocLock lock(m_objId);
		if(lock.lockStatus())
			return E_ACCESSDENIED;
		AcDbObjectPointer<AsdkRings> pC(m_objId,AcDb::kForWrite);
		If(pC.openStatus()!=Acad::eOk)
			return E_ACCESSDENIED;

		AcAxPoint3d pt(newVal);
		pC->setNormal(pt.asVector());
	}
	catch(const HRESULT hr)
	{
		Error("An error occurred. Check the input params.");
		return hr;
	}
	return S_OK;
}

If you have included the OPM feature into your project, read the topic 5.3 and then resume from here.

3.9 Additional Project Settings for Wrapper application.

Since you, calling member functions of your custom object this module will need to link with the import library of the DBX module implementing the custom object.
Add our custom object library full path into additional libraries path in the project settings tab. Select Project->Settings, and select the Link tab on the Project settings dialog.

Set the Category to Input and add “..\MyRings\debug\”. Make sure that you selected only RingsWrapper Project only.

NOTE:
Since you have both projects in our workspace, I recommend setting the dependencies of the project. You do this because that will ensure you that whenever you modify the custom object project then Developer Studio will compile that project first and links the generated library into your Com project.

To do that Select the Project->Dependency and make the ComCustCir project to depend on CustObj.
Select the “ComCustCirc” in Select project to modify combo box.
In addition, check CustObj in the Dependent list.

Be sure that you have included the axauto15.tlb path, (generally that will be in Acad.exe directory) in to the executable file list in Developer Studio. If not, You can do this in using the Tools->Option menu and then select Directories tab. Choose the Executable files item in the Show Directories for combo box, add the axauto15.tlb file full path in the 'Directories' list.

3.10 Build the com Wrapper application

Now you are ready to build the project. Build the project by selecting Build->Build AsdkRingsWrapper.arx

After successfully building the project, open the AutoCAD application and load the DBX application.

It is necessary to load the application once manually at first in oder to register itself. The ARX application calls DllRegisterServer() when it receives the kInitAppMsg. Once the COM server is registered, it is no longer necessary to load the ARX application manually each time when you want to call it via COM. The runtime system of COM will make sure that the ARX is loaded. Be careful, however, when your ARX application is loaded by the COM runtime your acrxEntyPoint() is not executed.

4 Testing the COM wrapper

If everything goes well let’s try to create our Rings object using Visual Basic for Applications.

The following VBA macro exercises the Ring object:

Public Sub test()
Dim o As Rings
Set o = ThisDrawing.ModelSpace.AddCustomObject("AsdkRings")
o.Radius = 5
Dim n(0 To 2) As Double
n(1) = 1
o.Normal = n
Dim c(0 To 2) As Double
o.Center = c
o.Rings = 4
End Sub

To do that enter “vbaide” in AutoCAD command prompt. Visual basic application is launched by that command.
Now from the Insert pull down menu select “insert module”. You will present with a module section. Set the cursor in module window and copy the above macro into it.

Before executing this macro, load the reference of AsdkRingWrapper to your vbaide project. To do that, pull down the Tools menu and select Reference menu item.

You will presented with the following dialog.

Find and select AsdkComRing library and check it. Click OK

Now you are ready to run the macro. Run the macro and you will get our Rings object in the AutoCAD Window.

5 Adding OPM feature.

5.1 ATL Object Wizard Properties - OPM tab settings

Select the OPM tab, Here it has two check boxes. Select the first check box to implement categorizing of properties,

5.2 Development of OPM properties.

If a custom object does not implement a COM object wrapper for itself, the AcAxGetIUnknownOfObject() function will generate a default wrapper that implement the methods of IAcadEntity or IAcadObject, depending on if the underlying object can be cast to an AcDbEntity. OPM then uses this object to display the Color, Layer, Line type, and Line weight properties, also known as the entity common properties. ICategorizeProperties, IPerPropertyBrowsing, and IOPMPropertyExtension are the 'flavoring' interfaces. For more details about these interfaces refer to Static OPM COM Interfaces in ObjectARX online documentation.

The main purpose of IOPMPropertyExpander Interface class is to allow one property to be broken out into several properties in the OPM

By checking these to check boxes will create following additional functions in your implementation class(i.e. Rings.cpp).

//IOPMPropertyExpander
STDMETHODIMP CRings::GetElementValue(
	/* [in] */ DISPID dispID,
	/* [in] */ DWORD dwCookie,
	/* [out] */ VARIANT * pVarOut)
{
    //TO DO: Implement this function.
    return E_NOTIMPL;
}


//IOPMPropertyExpander
STDMETHODIMP CRings::SetElementValue(
	/* [in] */ DISPID dispID,
	/* [in] */ DWORD dwCookie,
	/* [in] */ VARIANT VarIn)
{
    //TO DO: Implement this function.
    return E_NOTIMPL;
}
//IOPMPropertyExpander
STDMETHODIMP CRings::GetElementStrings( 
	/* [in] */ DISPID dispID,
	/* [out] */ OPMLPOLESTR __RPC_FAR *pCaStringsOut,
	/* [out] */ OPMDWORD __RPC_FAR *pCaCookiesOut)
{
    //TO DO: Implement this function.
    return E_NOTIMPL;
}
//IOPMPropertyExpander
STDMETHODIMP CRings::GetElementGrouping(
    /* [in] */ DISPID dispID,
	/* [out] */ short *groupingNumber)
{
    //TO DO: Implement this function.
    return E_NOTIMPL;
}
//IOPMPropertyExpander
STDMETHODIMP CRings::GetGroupCount(
    /* [in] */ DISPID dispID,
	/* [out] */ long *nGroupCnt)
{
    //TO DO: Implement this function.
    return E_NOTIMPL;
}

In the InterfaceSupportsErrorInfo() function four entries are added to static Const IDD8 arr[] array. They are IID_IOPMPropertyExpander, IID_ICategorizeProperties, IID_IPerPropertyBrowsing and IID_IOPMPropertyExtension. These four are specific to OPM.

STDMETHODIMP CRings::InterfaceSupportsErrorInfo(REFIID riid)
{
	static const IID* arr[] = 
	{
		&IID_IRings
        
        ,&IID_IAcadEntity
        
        
        ,&IID_IOPMPropertyExpander

        ,&IID_ICategorizeProperties
        ,&IID_IPerPropertyBrowsing
        ,&IID_IOPMPropertyExtension
        
	};
	for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		if (InlineIsEqualGUID(*arr[i],riid))
			return S_OK;
	}
	return S_FALSE;
}

In your header file i.e. in Rings.h you will notice some additional code. They are indicated by bold letters.

The class is additionally inherited from IOPMPropertyExtensionImpl and IOPMPropertyExpander.

class ATL_NO_VTABLE CRings : 
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CRings, &CLSID_Rings>,
public ISupportErrorInfo,
public IAcadObjectDispatchImpl<CRings,&CLSID_Rings,  IRings, &IID_IRings, &LIBID_AsdkRINGSWRAPPER1Lib>
,public IOPMPropertyExtensionImpl<CRings>
,public IOPMPropertyExpander  
{
        .  .  .  .  .  .
        .  .  .  .  .  .
};

There is also a few more COM interface maps.

BEGIN_COM_MAP(CRings)
COM_INTERFACE_ENTRY(IRings)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
COM_INTERFACE_ENTRY(IAcadBaseObject)
COM_INTERFACE_ENTRY(IAcadObject)
COM_INTERFACE_ENTRY(IRetrieveApplication)
COM_INTERFACE_ENTRY(IOPMPropertyExtension)
COM_INTERFACE_ENTRY(ICategorizeProperties)
COM_INTERFACE_ENTRY(IPerPropertyBrowsing)
COM_INTERFACE_ENTRY(IOPMPropertyExpander)
END_COM_MAP()

And one OPM map entry

// IOPMPropertyExtension
BEGIN_OPMPROP_MAP()
//TO DO: Use the OPMPROP_ENTRY macro for each of your properties
END_OPMPROP_MAP()

In addition, prototype declarations for useful functions.

    //IOPMPropertyExpander
	STDMETHOD(GetElementValue)(
		/* [in] */ DISPID dispID,
		/* [in] */ DWORD dwCookie,
		/* [out] */ VARIANT * pVarOut) ;
    //IOPMPropertyExpander
	STDMETHOD(SetElementValue)(
		/* [in] */ DISPID dispID,
		/* [in] */ DWORD dwCookie,
		/* [in] */ VARIANT VarIn) ;       
    //IOPMPropertyExpander
	STDMETHOD(GetElementStrings)( 
		/* [in] */ DISPID dispID,
		/* [out] */ OPMLPOLESTR __RPC_FAR *pCaStringsOut,
		/* [out] */ OPMDWORD __RPC_FAR *pCaCookiesOut) ;
    //IOPMPropertyExpander
    STDMETHOD(GetElementGrouping)(
        /* [in] */ DISPID dispID,
		/* [out] */ short *groupingNumber) ;
    //IOPMPropertyExpander
    STDMETHOD(GetGroupCount)(
        /* [in] */ DISPID dispID,
		/* [out] */ long *nGroupCnt) ;

These functions are called for each of the variable being processed. To find out on which kind of variable it is interested in processing use the argument type DISPID that is passed to each of these functions. This DISPID is the id of the property. For details about these functions refer IOPMPropertyExpander interface in Online ObjectARX help.

FUNCTION NAME PURPOSE
GetGroupCount Get the number of item in each element
GetElementGrouping Get the number of elements in the property
GetElementStrings Get the element string names
GetElementValue Get the element value
SetElementValue Set the element value

The following algorithm is used to display the point and vertices values in OPM. (DispId_of_Property is the DISPID for the property, Receive_VertexCount is the variable that receives the number of vertices from the COM wrapper.)

If(SUCCEEDED == GetElementStrings(DispId_of_Property, &opmlpstr, &opmdword))
{
	GetElementGrouping(DispId_of_Property, &grouping);

//If there is no grouping we just expand the property into opmlpstr.cElems
                //properties, otherwise we create a spinner control with the property name
                //and opmlpstr.cElems properties below it.
	if(0 == grouping )
{
	count = opmlpstr.cElems;
}
else
{
	addCount = grouping;
	create_spin_control();
	GetGroupCount(DispId_of_Property, &Receive_VertexCount);
}

Display_the_each _element();
}

5.3 Modify the OPM override functions

Modify the code in Rings implementation (Rings.cpp) for these functions as follows.

//IOPMPropertyExpander
STDMETHODIMP CRings::GetElementValue(
	/* [in] */ DISPID dispID,
	/* [in] */ DWORD dwCookie,
	/* [out] */ VARIANT * pVarOut)
{
    if (dispID == 2)
	{
		CComVariant var;
		get_center(&var);
		AcAxPoint3d pt(var);
		pVarOut->vt = VT_R8;
		pVarOut->dblVal = pt[dwCookie];
		return S_OK;
	} else if (dispID == 4)
	{
		CComVariant var;
		get_normal(&var);
		AcAxPoint3d pt(var);
		pVarOut->vt = VT_R8;
		pVarOut->dblVal = pt[dwCookie];
		return S_OK;
	}
    return E_NOTIMPL;
}
//IOPMPropertyExpander
STDMETHODIMP CRings::SetElementValue(
	/* [in] */ DISPID dispID,
	/* [in] */ DWORD dwCookie,
	/* [in] */ VARIANT VarIn)
{
    if (dispID == 2)
	{
		CComVariant var;
		get_center(&var);
		AcAxPoint3d pt(var);
		pt[dwCookie] = VarIn.dblVal;
		pt.setVariant(var);
		put_center(var);
		return S_OK;
	} else if (dispID == 4)
	{
		CComVariant var;
		get_normal(&var);
		AcAxPoint3d pt(var);
		pt[dwCookie] = VarIn.dblVal;
		pt.setVariant(var);
		put_normal(var);
		return S_OK;
	}
    return E_NOTIMPL;
}
//IOPMPropertyExpander
STDMETHODIMP CRings::GetElementStrings( 
	/* [in] */ DISPID dispID,
	/* [out] */ OPMLPOLESTR __RPC_FAR *pCaStringsOut,
	/* [out] */ OPMDWORD __RPC_FAR *pCaCookiesOut)
{
    if (dispID == 2)
	{
		pCaStringsOut->cElems = 3;
		pCaStringsOut->pElems = (LPOLESTR*)CoTaskMemAlloc(sizeof(LPOLESTR)*3);
		pCaStringsOut->pElems[0] = SysAllocString(L"Center X");
		pCaStringsOut->pElems[1] = SysAllocString(L"Center Y");
		pCaStringsOut->pElems[2] = SysAllocString(L"Center Z");
		pCaCookiesOut->cElems = 3;
		pCaCookiesOut->pElems = (DWORD*)CoTaskMemAlloc(sizeof(DWORD)*3);
		for (int i=0;i<3;i++)
			pCaCookiesOut->pElems[i] = i;
		return S_OK;
	} else if (dispID == 4)
	{
		pCaStringsOut->cElems = 3;
		pCaStringsOut->pElems = (LPOLESTR*)CoTaskMemAlloc(sizeof(LPOLESTR)*3);
		pCaStringsOut->pElems[0] = SysAllocString(L"Normal X");
		pCaStringsOut->pElems[1] = SysAllocString(L"Normal Y");
		pCaStringsOut->pElems[2] = SysAllocString(L"Normal Z");
		pCaCookiesOut->cElems = 3;
		pCaCookiesOut->pElems = (DWORD*)CoTaskMemAlloc(sizeof(DWORD)*3);
		for (int i=0;i<3;i++)
			pCaCookiesOut->pElems[i] = i;
		return S_OK;
	} 
    return E_NOTIMPL;
}




//IOPMPropertyExpander
STDMETHODIMP CRings::GetElementGrouping(
    /* [in] */ DISPID dispID,
	/* [out] */ short *groupingNumber)
{
	if (dispID == 2 || dispID == 4)
	{
		*groupingNumber = 0;
		return S_OK;
	}
	return E_NOTIMPL;
}
//IOPMPropertyExpander
STDMETHODIMP CRings::GetGroupCount(
    /* [in] */ DISPID dispID,
	/* [out] */ long *nGroupCnt)
{
    if (dispID == 2 || dispID == 4)
	{
		*nGroupCnt = 3;
		return S_OK;
	}
	return E_NOTIMPL;
}

In Addition, modify the code in Rings.h at the OPM property map section as follows:

// IOPMPropertyExtension
BEGIN_OPMPROP_MAP()
	OPMPROP_ENTRY(0, 0x00000001, PROPCAT_Geometry, 0, 0, 0, "", 0, 1, IID_NULL, IID_NULL, "")
	OPMPROP_ENTRY(0, 0x00000002, PROPCAT_Geometry, 0, 0, 0, "", 0, 1, IID_NULL, IID_NULL, "")
	OPMPROP_ENTRY(0, 0x00000003, PROPCAT_Geometry, 0, 0, 0, "", 0, 1, IID_NULL, IID_NULL, "")
	OPMPROP_ENTRY(0, 0x00000004, PROPCAT_Geometry, 0, 0, 0, "", 0, 1, IID_NULL, IID_NULL, "")
END_OPMPROP_MAP()

That’s all you have to do for supporting OPM.

posted @ 2014-08-01 10:53  kevinzhwl  阅读(1423)  评论(0编辑  收藏  举报