----哆啦劉小洋 原創,轉載需說明出處 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檔案,我的建議是都注釋掉或删除。也可以所有檔案都删除這句話,隻是無法使用接口需要用到的常量和枚舉定義。
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 使用便捷性
明顯接口方式大比分獲勝,第一是整個準備工作就是拷貝一個檔案,添加一行代碼。第二是接口使用便捷,接口方法傳回的接口是一個接口指針,可以實作連續指向,比如:
包裝類就很難啦!每個函數傳回的是一個分發接口LPDISPATCH,是以需要轉換,比如下代碼,CAcadAppliction類接口函數get_Documents傳回的就是一個LPDISPATCH,不信去看代碼。而docs呢,是CAcadDocuments類,是以必須要轉換,是以很難實作接口的連續指向。
并且,包裝類不同版本生成的檔案不同,還會出錯!可見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。