在AutoCAD绘图工作中,经常用到特性面板,它可以方便地查询、修改CAD对象的详细信息,如颜色、线型等,它是一个非常实用且便捷的工具。如果能为自定义对象添加一个特性面板(OPM),这无疑是众多初学者极感兴趣的事情。笔者也是一名ObjectARX的初学者和爱好者,为了给自定义对象添加OPM,笔者查阅了无数的网上和书籍资料,但大多寥寥数语,含糊其辞。功夫不负有心人,在经历了千百次的“测试-失败-改进”后,终于摸索出了自定义对象OPM编程的部分细节,现将之分享。由于笔者水平有限,文中的错误和疏漏还望各位大侠指点。
本文以具有6个顶点的折线及一行格式化文字组成一个自定义对象(MyObject),拟在特性面板中实现折线顶点的显示与编辑、格式化文本的选择以及位置编辑等功能。笔者所用版本为ObjectARX 2020,编程语言为C++。本例功能虽简单,但特性表中的一般项、下拉列表控件、数值调节控件、三维坐标分解等均有涉及,基本可以涵盖特性表的全部常用功能。
- 添加新建项,双击“ArxAtlWizComWrapper”;
- Shot Name填写“MyObjectOPM”,其他默认,点击下一步;
- DBX classname填写自定义对象的类名“MyObject”,并选中“Entity Interfafe support(versus just Object)”、“Use IOPMPropertyExtensionImpl”、“Implement IOPMPropertyExpander”选项,点击完成;
- 打开“MyObjectOPM.h”文件,将#include "***.h"(DBX类的头文件)修改为#include "***_i.h",并添加包含#include "MyObject.h";
- 重新生成一次。
- 为自定义对象类添加函数:virtual Acad::ErrorStatus subGetClassID(CLSID* pClsid) const(protected属性),其实现代码如下:
//获取OPM的ID,以建立COM方式的通讯
Acad::ErrorStatus MyObject::subGetClassID(CLSID* pClsid) const
{
assertReadEnabled();
*pClsid = CLSID_MyObjectOPM;
return Acad::eOk;
}
7. 变量CLSID_MyObjectOPM会报错,添加4中的包含文件:#include "***_i.h"
8. 为自定义对象类添加私有变量:
private:
AcGePoint3d mTxPt; //格式化文本插入点
AcGePoint3dArray mPts; //折线的顶点坐标集合
AcString mTx; //格式化文本
9. 为自定义对象类添加公有函数:
//-----------------------------------------------------------------------------
Acad::ErrorStatus MyObject::GetVertex(int index, AcGePoint3d &pt) const
{
assertReadEnabled();
pt = mPts[index];
return Acad::eOk;
}
//-----------------------------------------------------------------------------
Acad::ErrorStatus MyObject::SetVertex(int index, AcGePoint3d newVal)
{
assertWriteEnabled();
mPts[index] = newVal;
return Acad::eOk;
}
//-----------------------------------------------------------------------------
AcString MyObject::GetText() const
{
assertReadEnabled();
return mTx;
}
//-----------------------------------------------------------------------------
Acad::ErrorStatus MyObject::SetText(AcString newVal)
{
assertWriteEnabled();
mTx = newVal;
return Acad::eOk;
}
//-----------------------------------------------------------------------------
Acad::ErrorStatus MyObject::GetIns(AcGePoint3d &pt) const
{
assertReadEnabled();
pt = mTxPt;
return Acad::eOk;
}
//-----------------------------------------------------------------------------
Acad::ErrorStatus MyObject::SetIns(AcGePoint3d newVal)
{
assertWriteEnabled();
mTxPt = newVal;
return Acad::eOk;
}
10. 打开DBX项目的.idl文件,在interface IMyObjectOPM : IAcadEntity { };段中添加如下代码,以初始化特性表:
[propget, id(1), helpstring("折线顶点坐标集合")] HRESULT Vertex([in] SHORT index, [out, retval] VARIANT *pVal);
[propput, id(1), helpstring("折线顶点坐标集合")] HRESULT Vertex([in] SHORT index, [in] VARIANT newVal);
[propget, id(2), helpstring("格式化的文本")] HRESULT Explain([out, retval] BSTR *pVal);
[propput, id(2), helpstring("格式化的文本")] HRESULT Explain([in] BSTR *newVal);
[propget, id(3), helpstring("文本插入点")] HRESULT InsertPt([out, retval] VARIANT *pVal);
[propput, id(3), helpstring("文本插入点")] HRESULT InsertPt([in] VARIANT newVal);
[propget, id(4), helpstring("说明")] HRESULT Note([out, retval] BSTR *pVal);
//注意:id值从1开始自定义编号,既是在“特性”列表中的显示顺序(dispID),但此值在所有代码中须始终一一对应。
11. 打开“MyObjectOPM.h”文件,定义2个常量:#define WjcCategoryID1 86 #define WjcCategoryID2 87//用于特性表的分类号,在BEGIN_OPMPROP_MAP() END_OPMPROP_MAP()段间插入如下语句,以进行属性分类及编辑模式等的声明,这些语句须和10中的语句对应:
//IOPMPropertyExtension
BEGIN_OPMPROP_MAP()
OPMPROP_ENTRY(0, 0x001, WjcCategoryID1, 0, 0, 0, _T(""), 0, 1, IID_NULL, IID_NULL, "")
OPMPROP_ENTRY(0, 0x002, WjcCategoryID2, 0, 0, 0, _T(""), 0, 1, IID_NULL, IID_NULL, "")
OPMPROP_ENTRY(0, 0x003, WjcCategoryID2, 0, 0, 0, _T(""), 0, 1, IID_NULL, IID_NULL, "")
OPMPROP_ENTRY(0, 0x004, WjcCategoryID1, 0, 0, 0, _T(""), 0, 0, IID_NULL, IID_NULL, "")
END_OPMPROP_MAP()
/*⑴DescriptionID:默认0
⑵dispID:见10条的注意
⑶catagoryID:分组的ID
⑷catagoryNameID:默认0
⑸elements string list ID (semi-colon separator) :默认0
⑹predefined strings ID (semi-colon separator) :默认0
⑺predefined values:默认_T("")
⑻grouping:默认0
⑼editable:是否可编辑:1-可编辑;0-不能编辑
⑽property:默认 IID_NULL
⑾other:默认 IID_NULL
⑿proppage:默认 ""*/
12. 声明如下函数,这些函数须和10中的语句一一对应,且函数名称、参数都是一一对应的,这些函数是OPM和自定义对象进行数据交换的唯一通道:
public:
//IMyObjectOPM
STDMETHOD(get_Vertex)(/*[in]*/ SHORT index, /*[out, retval]*/VARIANT *pVal);
STDMETHOD(put_Vertex)(/*[in]*/ SHORT index, /*[in]*/ VARIANT newVal);
STDMETHOD(get_Explain)(BSTR *pVal);
STDMETHOD(put_Explain)(BSTR *newVal);
STDMETHOD(get_InsertPt)(/*[out, retval]*/VARIANT *pVal);
STDMETHOD(put_InsertPt)(/*[in]*/ VARIANT newVal);
STDMETHOD(get_Note)(BSTR *pVal);
//在MyObject类中须有与这些函数对应的函数
13. 实现上述函数:
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::get_Vertex(/*[in]*/ SHORT index, /*[out, retval]*/VARIANT *pVal)
{
try
{
AcDbObjectPointer <MyObject>pC(m_objRef.objectId(), AcDb::kForRead);
if (pC.openStatus() != Acad::eOk)
return E_ACCESSDENIED;
AcAxPoint3d pt;
pC->GetVertex(index, pt);
pt.setVariant(*pVal);
}
catch (const HRESULT hf)//
{
Error("请检查输入的参数!");
return hf;
}
return S_OK;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::put_Vertex(/*[in]*/ SHORT index, /*[in]*/ VARIANT newVal)
{
try
{
AcAxDocLock docLock(m_objRef.objectId(), AcAxDocLock::kNormal);
if (docLock.lockStatus() != Acad::eOk && docLock.lockStatus() != Acad::eNoDatabase)
return E_ACCESSDENIED;
AcDbObjectPointer <MyObject>pC(m_objRef.objectId(), AcDb::kForWrite);
if (pC.openStatus() != Acad::eOk)
{
return E_ACCESSDENIED;
}
AcAxPoint3d pt(newVal);
pC->SetVertex(index, pt);
}
catch (const HRESULT hf)
{
Error("请检查输入的参数!");
return hf;
}
return S_OK;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::get_Explain(BSTR *pVal)
{
try
{
AcDbObjectPointer <MyObject>pC(m_objRef.objectId(), AcDb::kForRead);
if (pC.openStatus() != Acad::eOk)
{
return E_ACCESSDENIED;
}
*pVal = ::SysAllocString(pC->GetText());
}
catch (const HRESULT hf)
{
Error("请检查输入的参数!");
return hf;
}
return S_OK;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::put_Explain(BSTR *newVal)
{
try
{
AcString ts;
ts.format(_T("%s"), *newVal);
::SysFreeString(*newVal);
AcAxDocLock docLock(m_objRef.objectId(), AcAxDocLock::kNormal);
if (docLock.lockStatus() != Acad::eOk && docLock.lockStatus() != Acad::eNoDatabase)
return E_ACCESSDENIED;
AcDbObjectPointer <MyObject>pC(m_objRef.objectId(), AcDb::kForWrite);
if (pC.openStatus() != Acad::eOk)
{
return E_ACCESSDENIED;
}
pC->SetText(ts);
}
catch (const HRESULT hf)
{
Error("请检查输入的参数!");
return hf;
}
return S_OK;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::get_InsertPt(/*[out, retval]*/VARIANT *pVal)
{
try
{
AcDbObjectPointer <MyObject>pC(m_objRef.objectId(), AcDb::kForRead);
if (pC.openStatus() != Acad::eOk)
return E_ACCESSDENIED;
AcAxPoint3d pt;
pC->GetIns(pt);
pt.setVariant(*pVal);
}
catch (const HRESULT hf)//
{
Error("请检查输入的参数!");
return hf;
}
return S_OK;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::put_InsertPt(/*[in]*/ VARIANT newVal)
{
try
{
AcAxDocLock docLock(m_objRef.objectId(), AcAxDocLock::kNormal);
if (docLock.lockStatus() != Acad::eOk && docLock.lockStatus() != Acad::eNoDatabase)
return E_ACCESSDENIED;
AcDbObjectPointer <MyObject>pC(m_objRef.objectId(), AcDb::kForWrite);
if (pC.openStatus() != Acad::eOk)
{
return E_ACCESSDENIED;
}
AcAxPoint3d pt(newVal);
pC->SetIns(pt);
}
catch (const HRESULT hf)
{
Error("请检查输入的参数!");
return hf;
}
return S_OK;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::get_Note(BSTR *pVal)
{
*pVal = ::SysAllocString(_T("成功展示"));
return S_OK;
}
14. 在“MyObjectOPM.h”中声明如下重载函数(public):
//IOPMPropertyExpander
//分解复杂的属性(如三维坐标值包含x、y、z属性)
STDMETHOD(GetElementStrings)(/*[in]*/DISPID dispID, /*[out]*/OPMLPOLESTR __RPC_FAR *pCaStringsOut, /*[out]*/OPMDWORD __RPC_FAR *pCaCookiesOut);
//设置复杂属性的当前值
STDMETHOD(GetElementValue)(/*[in]*/DISPID dispID, /*[in]*/DWORD dwCookie, /*[out]*/VARIANT *pVarOut);
//读取复杂属性的当前值,并返回给关联的实体
STDMETHOD(SetElementValue)(/*[in]*/DISPID dispID, /*[in]*/DWORD dwCookie, /*[in]*/VARIANT VarIn);
//设置数值调节钮控件每组数据的数据个数,如三维坐标的每组数据个数为3,即x、y、z
STDMETHOD(GetElementGrouping)(/*[in]*/DISPID dispID, /*[out]*/short *groupingNumber);
//设置数值调节钮控件的最大值,其值从1开始,步长为1
STDMETHOD(GetGroupCount)(/*[in]*/DISPID dispID, /*[out]*/long *nGroupCnt);
//设置下拉列表控件中的预定义字符串(或true/false)
STDMETHOD(GetPredefinedStrings)(/*[in]*/DISPID dispID, /*[out]*/CALPOLESTR *pCaStringsOut, /*[out]*/CADWORD *pCaCookiesOut);
//设置下拉列表控件的当前显示字符串(或true/false)
STDMETHOD(GetPredefinedValue)(/*[in]*/DISPID dispID, /*[in]*/DWORD dwCookie, /*[out]*/VARIANT *pVarOut);
//设置列表分类/分组的名称
STDMETHOD(GetCategoryName)(/* [in] */ PROPCAT propcat,/* [in] */ LCID lcid,/* [out] */ BSTR* pbstrName);
//重置/覆盖列表中简单属性(单行显示)的名称
STDMETHOD(GetDisplayName)(/*[in]*/DISPID dispID, /*[out]*/BSTR *pBstr);
15. 实现上述函数:
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetElementValue (DISPID dispID, DWORD dwCookie, VARIANT *pVarOut) {
if (pVarOut == NULL) return (E_POINTER);
if (dispID == 1)
{
int index;
index = int(dwCookie / 3);/*dwCookie是一个计数器,是用于获取和设置每个属性项的值的唯一标识符,编号从0开始,依次递增1.本例中一个三维坐标对应3个连续的dwCookie值*/
CComVariant var;
get_Vertex(index, &var);
AcAxPoint3d pt(var);
pVarOut->vt = VT_R8;
pVarOut->dblVal = pt[dwCookie % 3];
return (S_OK);
}
else if (dispID == 3)
{
CComVariant var;
get_InsertPt(&var);
AcAxPoint3d pt(var);
pVarOut->vt = VT_R8;
pVarOut->dblVal = pt[dwCookie];
return (S_OK);
}
return (E_NOTIMPL) ;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::SetElementValue (DISPID dispID, DWORD dwCookie, VARIANT VarIn) {
if (dispID == 1)
{
int index;
index = int(dwCookie / 3);
CComVariant var;
get_Vertex(index, &var);
AcAxPoint3d pt(var);
pt[dwCookie % 3] = VarIn.dblVal;
pt.setVariant(var);
put_Vertex(index, var);
return (S_OK);
}
else if (dispID == 3)
{
CComVariant var;
get_InsertPt(&var);
AcAxPoint3d pt(var);
pt[dwCookie] = VarIn.dblVal;
pt.setVariant(var);
put_InsertPt(var);
return (S_OK);
}
return (E_NOTIMPL) ;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetElementStrings (DISPID dispID, OPMLPOLESTR __RPC_FAR *pCaStringsOut, OPMDWORD __RPC_FAR *pCaCookiesOut) {
if (pCaStringsOut == NULL || pCaCookiesOut == NULL) return (E_POINTER);
if (dispID == 1)
{
pCaStringsOut->cElems = 3;
pCaStringsOut->pElems = (LPOLESTR*)CoTaskMemAlloc(sizeof(LPOLESTR) * 3);
pCaStringsOut->pElems[0] = SysAllocString(L" 顶点 x 坐标");
pCaStringsOut->pElems[1] = SysAllocString(L" 顶点 y 坐标");
pCaStringsOut->pElems[2] = SysAllocString(L" 顶点 z 坐标");
pCaCookiesOut->cElems = 3;
pCaCookiesOut->pElems = (DWORD*)CoTaskMemAlloc(sizeof(DWORD) * 3);
pCaCookiesOut->pElems[0] = 0;
pCaCookiesOut->pElems[1] = 1;
pCaCookiesOut->pElems[2] = 2;
return (S_OK);
}
else if (dispID == 3)
{
pCaStringsOut->cElems = 3;
pCaStringsOut->pElems = (LPOLESTR*)CoTaskMemAlloc(sizeof(LPOLESTR) * 3);
pCaStringsOut->pElems[0] = SysAllocString(L"文本 x 坐标");
pCaStringsOut->pElems[1] = SysAllocString(L"文本 y 坐标");
pCaStringsOut->pElems[2] = SysAllocString(L"文本 z 坐标");
pCaCookiesOut->cElems = 3;
pCaCookiesOut->pElems = (DWORD*)CoTaskMemAlloc(sizeof(DWORD) * 3);
pCaCookiesOut->pElems[0] = 0;
pCaCookiesOut->pElems[1] = 1;
pCaCookiesOut->pElems[2] = 2;
return (S_OK);
}
return (E_NOTIMPL) ;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetElementGrouping (DISPID dispID, short *groupingNumber) {
if (groupingNumber == NULL) return (E_POINTER);
if (dispID == 1)
{
*groupingNumber = 3;//每组三个数据,及x、y、z
return (S_OK);
}
return (E_NOTIMPL) ;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetGroupCount (DISPID dispID, long *nGroupCnt) {
if (nGroupCnt == NULL)return (E_POINTER);
if (dispID == 1)
{
*nGroupCnt = 6; // 顶点数
return S_OK;
}
return (E_NOTIMPL) ;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetPredefinedStrings(/*[in]*/DISPID dispID, /*[out]*/CALPOLESTR *pCaStringsOut, /*[out]*/CADWORD *pCaCookiesOut)
{
if (dispID != 2)
return IOPMPropertyExtensionImpl<CMyObjectOPM>::GetPredefinedStrings(dispID, pCaStringsOut, pCaCookiesOut);
USES_CONVERSION;
if (dispID == 2)
{
long size = 4;
pCaStringsOut->pElems = (LPOLESTR *)::CoTaskMemAlloc(sizeof(LPOLESTR) * size);
pCaCookiesOut->pElems = (DWORD *)::CoTaskMemAlloc(sizeof(DWORD) * size);
pCaStringsOut->cElems = (ULONG)size;
pCaCookiesOut->cElems = (ULONG)size;
pCaStringsOut->pElems[0] = ::SysAllocString(_T("我们"));
pCaCookiesOut->pElems[0] = 0;
pCaStringsOut->pElems[1] = ::SysAllocString(_T("一起"));
pCaCookiesOut->pElems[1] = 1;
pCaStringsOut->pElems[2] = ::SysAllocString(_T("学习"));
pCaCookiesOut->pElems[2] = 2;
pCaStringsOut->pElems[3] = ::SysAllocString(_T("ObjectARX"));
pCaCookiesOut->pElems[3] = 3;
return S_OK;
}
return E_NOTIMPL;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetPredefinedValue(/*[in]*/DISPID dispID, /*[in]*/DWORD dwCookie, /*[out]*/VARIANT *pVarOut)
{
if (dispID != 2)
return IOPMPropertyExtensionImpl<CMyObjectOPM>::GetPredefinedValue(dispID, dwCookie, pVarOut);
USES_CONVERSION;
const TCHAR* pName = NULL;
if (dispID == 2)
{
switch (dwCookie)
{
case 0:
pName = _T("我们");
break;
case 1:
pName = _T("一起");
break;
case 2:
pName = _T("学习");
break;
case 3:
pName = _T("ObjectARX");
break;
default:
return E_NOTIMPL;
}
::VariantCopy(pVarOut, &CComVariant(CT2W(pName)));
return S_OK;
}
return E_NOTIMPL;
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetCategoryName(/* [in] */ PROPCAT propcat,/* [in] */ LCID lcid,/* [out] */ BSTR* pbstrName)
{
if (propcat == WjcCategoryID1)
{
*pbstrName = ::SysAllocString(_T("分类1 测试"));
return S_OK;
}
else if (propcat == WjcCategoryID2)
{
*pbstrName = ::SysAllocString(_T("分类2 测试"));
return S_OK;
}
else
{
return S_FALSE;
}
}
//-----------------------------------------------------------------------------
STDMETHODIMP CMyObjectOPM::GetDisplayName(/*[in]*/DISPID dispID, /*[out]*/BSTR *pBstr)
{
if (pBstr == NULL) return (E_POINTER);
switch (dispID)
{
case (0x401):
*pBstr = ::SysAllocString(_T("对象特性表-乱石作品"));
break;
case (0x516):
*pBstr = ::SysAllocString(_T("颜色"));
break;
case (0x501):
*pBstr = ::SysAllocString(_T("图层"));
break;
case (0x502):
*pBstr = ::SysAllocString(_T("线型"));
break;
case (0x503):
*pBstr = ::SysAllocString(_T("线型比例"));
break;
case (0x513):
*pBstr = ::SysAllocString(_T("打印样式"));
break;
case (0x514):
*pBstr = ::SysAllocString(_T("线宽"));
break;
case (0x515):
*pBstr = ::SysAllocString(_T("超链接"));
break;
case (0x577):
*pBstr = ::SysAllocString(_T("材质"));
break;
case (0x579):
*pBstr = ::SysAllocString(_T("实体透明度"));
break;
case (0x01):
*pBstr = ::SysAllocString(_T("当前顶点"));
break;
case (0x02):
*pBstr = ::SysAllocString(_T("格式文本"));
break;
case (0x04):
*pBstr = ::SysAllocString(_T("说明"));
break;
default:
break;
}
return (S_OK);
}
16. 重新编译后,在AutoCAD中的测试结果如图,此时选中自定义对象,特性表中已能显示出所需参数,且更改特性表中的参数,自定义对象亦能随之变化。成功!!!