----哆啦刘小洋 原创,转载需说明出处 2022-12-29
VC调用AutoCAD自动化
- 1 简介
- 2 AutoCAD的Automation类型库说明文件
- 3 包装类方式
- 3.1 VC6
- 3.2 VC2022
- 4 接口调用方式
- 5 两种方式对比
- 5.1 使用便捷性
- 5.2 兼容性
- 5.3 结论
- 6 后记
1 简介
在工程领域,使用AutoCAD时经常利用自动化(Automation)技术提高工作效率,VC调用AutoCAD自动化时有两种常用的方式,一个是包装类的方式,另一个是接口的方式,当然不只是AutoCAD,一般的支持Automation的软件都可以使用这两种方式,本文就这两种方式给出详细的使用方法,并简要总结各自的优缺点。文中代码基于VC6及以上。
2 AutoCAD的Automation类型库说明文件
不管是用包装类方式还是接口方式,编程时都需要用到类型库说明文件(.tlb),这个文件是微软Com(Component)技术的一个标准文件,有点类似于c++的声明头文件,文件中给出了所有的接口声明、常量定义、枚举声明等等,方便使用者调用,但这个文件是二进制文件,无法直观的查看内容。其实Com还有一种接口声明的标准文件“.tlh”,这种文件是文本格式的,可以直接查看。不过AutoCAD提供的就是tlb格式。
AutoCAD在安装后,提供了组件的接口文件,一般型如:acax16chs.tlb文件,文件名中的16是AutoCAD的内部版本号,比如AutoCAD2016的内部版本号是20;文件名中的chs是语言,chs就是简体中文,我们一般都用这个。这个tlb文件的位置也和Windows版本以及AutoCAD的版本有关,比如“C:\Program Files\Common Files\Autodesk Shared”,不行就先用文件搜索找到位置再来。
3 包装类方式
包装类是VC对Automation组件的接口进行类封装,然后使用时,把这些类当作普通的类使用即可。需要说明,VC不同的版本对同一个tlb文件的包装结果是不一样的,甚至会差别非常大。据我的使用经验,至少有几次版本有断代式的明显差异。这里我就以VC6和VC2022两个版本分别给出使用方法。
3.1 VC6
通过工程中任意一个类进入类向导窗口,在右侧的AddClass里选择“From a type library”,如下图:
接下来会打开文件对话框让你选择,我们选择acax??chs.tlb,??是AutoCAD的版本号。
打开后,会让你确认要包装哪些接口类:
一般全选即可。(先点中第一个,然后滚动到最后按住Shift点中最后一个)。这里可以对h和cpp文件命名或指定目录。再OK,VC6会生成那两个文件。同时,VC6的ClassView中会出现一大堆新的I开头的类,这些就是AutoCAD的接口包装类。
使用时,代码如下:
#include "acax20chs.h"
void OnTest_class_vc6()
{
CoInitialize(NULL);
IAcadApplication app;
CLSID clsid;
HRESULT h=NOERROR;
IUnknown* pUnknown;
LPDISPATCH pDispatch;
BOOL bSuccessget_Object = FALSE;
//获取AutoCAD的Class ID
h = ::CLSIDFromProgID(OLESTR("AutoCad.Application"), &clsid);
if(!SUCCEEDED(h))
return;
//获取正在运行的AutoCAD对象
h = GetActiveObject(clsid, NULL, &pUnknown);
if(SUCCEEDED(h))
{
h = pUnknown->QueryInterface(IID_IDispatch, (void**)&pDispatch);
if(SUCCEEDED(h))
{
bSuccessget_Object = TRUE;
pDispatch->Release();
app.AttachDispatch(pDispatch);
}
}
//没有正在运行的AutoCAD对象,就创建一个
if(!bSuccessget_Object)
h=app.CreateDispatch(clsid);
if(FAILED(h) || app.m_lpDispatch == NULL)
return;
app.SetVisible(TRUE);
//...
}
3.2 VC2022
在解决方案资源管理器栏,选中你的目标项目,点击右键,选择“添加 > 新建项”,如果是英文版,在Solution Explore栏,选择“ Add > New Item”:
注意,在VC2022中,一定要在解决方案资源管理器(Solution Explore)栏,类视图中点击项目右键是没有这个选项的。这和以前一些版本不同。
在添加新项对话框中,选择“已安装”下的“Visual C++”下面的“MFC”,在右面子项中,选择“TypeLib中的MFC类”(英文版:选择“Installed”下的“Visual C++”下面的“MFC”,在右面子项中,选择“MFC Class from Typelib”)。
选择好后,点击“添加”:
如上图所示,选择文件方式,再选择tlb文件(如“acax16chs.tlb”),在可用接口栏,一般点击“>>"全部选择,再确定。这时,VC2022会生成各个类的包装文件,和VC6最不一样的是,VC2022每个类对应一个文件,因此会有很多文件。在生成的时候等待时间长一点,不要做动作,否则可能出现文件没有内容的隐含错误。
使用时,也和VC6不太一样了,因为文件多了嘛。代码如下:
#include "CAcadApplication.h"
#include "CAcadDocuments.h"
#include "CAcadDocument.h"
#include "CAcadModelSpace.h"
void OnTest_class_vc2022()
{
CoInitialize(NULL);
CLSID clsid;
HRESULT h;
//获取Class ID
h = CLSIDFromProgID(OLESTR("AutoCad.Application"), &clsid);
if (!SUCCEEDED(h))
return;
CAcadApplication app;
IUnknown* pUnknown;
LPDISPATCH pDispatch;
BOOL bSuccessGetObject;
//获取正在运行的对象
bSuccessGetObject = FALSE;
h = ::GetActiveObject(clsid, NULL, &pUnknown);
if (SUCCEEDED(h))
{
h = pUnknown->QueryInterface(IID_IDispatch, (void**)&pDispatch);
if (SUCCEEDED(h))
{
bSuccessGetObject = TRUE;
pDispatch->Release();
app.AttachDispatch(pDispatch);
}
}
//没有正在运行的对象,就创建一个
if (!bSuccessGetObject)
h = app.CreateDispatch(clsid);
if (!SUCCEEDED(h) || app.m_lpDispatch == NULL)
return;
app.put_Visible(TRUE);
app.put_WindowState(3/*acMax*/);
CAcadDocuments docs;
CAcadDocument doc;
CAcadModelSpace ms;
//新建一个AutoCAD文档
docs = (CAcadDocuments)app.get_Documents();
doc = (CAcadDocument)docs.Add(vtMissing);
//模型空间
ms = (CAcadModelSpace)doc.get_ModelSpace();
//添加一条多义线
//ms.AddPolyline(...)
}
如果运气不好,编译会出现错误:error C2059: 语法错误:“,” 。神奇吧!竟然是包装类中出了错,比如下面这个函数。而且,不同VC版本,每次生成的文件错误还不一样(但VC6很稳定),所以,这里强调一下,在VC生成类文件的时候一定不要动,等个30秒。
__int64 get_HWND()
{
__int64 result;
InvokeHelper(0x2c, DISPATCH_PROPERTYGET, , (void*)&result, nullptr);//这一句,两个","间少了参数
return result;
}
下面列举了一些我碰到的错误和解决方法:
CAcadAppliction::get_HWND()和CAcadDocument::get_HWND()有错误,类型名添加一个VT_I8。
InvokeHelper(0x2c, DISPATCH_PROPERTYGET, , (void*)&result, nullptr); //原来
InvokeHelper(0x2c, DISPATCH_PROPERTYGET, VT_I8, (void*)&result, nullptr);//修改
InvokeHelper(0x40, DISPATCH_PROPERTYGET, , (void*)&result, nullptr); //原来
InvokeHelper(0x40, DISPATCH_PROPERTYGET, VT_I8, (void*)&result, nullptr);//修改
CAcadDocuments::get__NewEnum()有错误,类型名添加一个VT_UNKNOWN。
InvokeHelper(0xfffffffc, DISPATCH_PROPERTYGET, , (void*)&result, nullptr); //原来
InvokeHelper(0xfffffffc, DISPATCH_PROPERTYGET, VT_UNKNOWN, (void*)&result, nullptr);//修改
还可能有别的一些缺少参数类型的错误,修改方法也很简单,比如错误代码如下:
InvokeHelper(0x405, DISPATCH_PROPERTYGET, VT_I8, (void*)&result, nullptr); __int64 get_ObjectID()
{
__int64 result;//看这里
InvokeHelper(0x405, DISPATCH_PROPERTYGET, , (void*)&result, nullptr);
return result;
}
看函数第一行声明的是什么类型,就在第二句缺少类型参数的地方添加相应的类型。如果你不熟悉VARIANT,一般按下面的替换方式:
__int64 -> VT_I8 long -> VT_I4 LPUNKNOWN->VT_UNKNOWN 基本上可以应付了。其实看看别的函数也可以现学现卖解决问题。
此外,每个文件前面有一行类似下面的语句:
#import “C:\Program Files\Common Files\Autodesk Shared\acax20chs.tlb” no_namespace
建议第一,把acax20chs.tlb拷贝到工程文件中来,第二,只保留CAcadApplication.h文件中的这句话,并改成如下所示的代码。其他需要用到的h文件,我的建议是都注释掉或删除。也可以所有文件都删除这句话,只是无法使用接口需要用到的常量和枚举定义。
#import "acax20chs.tlb" no_namespace
4 接口调用方式
好了,包装类的方式总算好了。现在介绍另一种方式,接口调用方式对不同VC版本都是一样的使用。
这个方式不需要先去生成包装类,直接把AutoCAD提供的acax20chs.tlb拷贝到你工程来。拷贝过来的目的是避免AutoCAD被卸载或换了版本,就找不到文件了。代码如下:
#import "acax20chs.tlb" no_namespace named_guids
void OnTest_Interface()
{
CoInitialize(NULL);
IAcadApplicationPtr pApp;
IAcadDocumentsPtr pDocs;
IAcadDocumentPtr pDoc;
IAcadModelSpacePtr pMs;
HRESULT h = NOERROR;
CLSID clsid;
//获取Class ID
h = ::CLSIDFromProgID(OLESTR("AutoCad.Application"), &clsid);
if (FAILED(h))
return;
//获取正在运行的对象
h = pApp.GetActiveObject(clsid);
if (!SUCCEEDED(h))
{
//没有正在运行的对象,就创建一个
h = pApp.CreateInstance(clsid, NULL, CLSCTX_ALL);
if (FAILED(h))
return;
}
pApp->WindowState = AcWindowState::acMax;
pApp->Visible = VARIANT_TRUE;
//新建一个AutoCAD文档
pApp->get_Documents(&pDocs);
pDoc = pDocs->Add();
//模型空间
pDoc->get_ModelSpace(&pMs);
//添加一条多义线
//pMs->AddPolyline(...)
}
接口方式和包装类的方式差异甚大,这里大概解释一下:
1)为什么简单一句#import,后面的IAcadApplicationPtr类型、AcWindowState::acMax常量定义就都有了呢,也没有看到我们包含什么头文件啊。这是因为#import,VC会生成Com的另外一种标准文件.tlh和.tli,一个是接口声明,一个是接口实现代理,这两个文件是文本文件,在输出目录里面,可以方便的查看各种接口定义、常量定义。所以我们才能方便的使用IAcadApplicationPtr来定义变量等等。
2)型如IAcadApplicationPtr的类型是生成的接口智能指针,这种方式就是用接口指针来调用AutoCAD的Automation接口。
此外,代码中在模型空间添加一条多义线被注释了,是因为使用没有那么简单,这里要牵涉到变体数组的问题,可以参考我的另一篇文章:”SegeX SgxVariant:VC封装支持多维数组的变体类型“,里面提供了变体数组的封装代码。
5 两种方式对比
5.1 使用便捷性
明显接口方式大比分获胜,第一是整个准备工作就是拷贝一个文件,添加一行代码。第二是接口使用便捷,接口方法返回的接口是一个接口指针,可以实现连续指向,比如:
pApp->GetActiveDocument()->GetModelSpace()->AddPolyline(...)
包装类就很难啦!每个函数返回的是一个分发接口LPDISPATCH,因此需要转换,比如下代码,CAcadAppliction类接口函数get_Documents返回的就是一个LPDISPATCH,不信去看代码。而docs呢,是CAcadDocuments类,所以必须要转换,因此很难实现接口的连续指向。
docs = (CAcadDocuments)app.get_Documents();
并且,包装类不同版本生成的文件不同,还会出错!可见MFC对OLE的态度。
5.2 兼容性
兼容性恐怕比便捷性重要得多。因为便捷性不好嘛,只是开发人员一时的痛哭,但如果用户使用你的软件,却出现各种打不开、死机,那就要命了,可能职位不保!
接口方式的兼容性差。第一,接口可能是区分32位和64位的(不同软件支持不同),比如程序#import的是一个32位AutoCAD自带的tlb文件,那么一般情况下,你的程序只能在32位AutoCAD中运行良好,为了保持兼容,你需要编写两个版本的代码,根据目标计算机自动做出选择。同时,接口还可能区分AutoCAD版本的,也就是说,如果程序#import的是AutoCAD2012,用户计算机是AutoCAD2023的话,那么可能会出错!为了兼容,你还得编写多个版本的AutoCAD版本,至于哪些兼容,哪些不兼容,我也没有统计过。
5.3 结论
因此,我建议采用包装类的方式。
6 后记
一般认为,Com技术,特别是OLE技术,对于一般开发人员而言过时了,我认为,确实过时了。第一,Com技术仅能良好的用于Windwos,不能适应现在的多平台融合的需求;第二,Com技术理念很好,但使用时要求的技术相对较高,特别是用VC,如果你想编写一个支持OLE和Automation让别人调用你的程序,那是难上加难(也可以看我的文章系列:”SegeX Automation:VC程序的Automation支持(让你的程序既是应用程序,又具备Automation服务器的功能 可被别的程序调用,类似Word AutoCAD)“)(还没写好)。但我想说的不是这个,我想说的是Automation技术有时会大大提高工作效率和图件的准确性,这对于很多工程人员来讲,实在是太重要了。举个真实例子,2007年左右,某单位因为一个施工图件在引用勘探资料时,勘探资料比例尺标注错了,注意只是标注错了,显然是拷贝别的图件没有修改全,从而引起施工图不准确,导致施工中大面积变更,直接经济损失超过1个亿(就是一个数字错了,1写成了2),而如果是AutoCAD图件自动化处理,取代人工成图和过程中的手工转移引用,就可以避免此类错误。回到正题,这么重要的技术,虽然落魄了,但你得给一个新的替代技术呀,不然成天只知道叫过时了,过时了,有什么用呢。当然,话说回来,AutoCAD功能强大,它提供了多种开发手段,不一定用Automation技术,比如script、AutoLisp、Arx,还有基于.Net的开发组件,这个我打算另外写文章再专门介绍。但除了AutoCAD,别的很多软件二次开发最多也就支持Automation。