-
使用MFC(VS2010)開發ArcGIS Engine 10.1
網上C#結合ArcGIS Engine的資料簡直太多了,多的都無法形容,但是C++的卻很少,前一段時間不斷的有人問在VC中如何開發ArcGIS Engine,說實話我幾乎沒怎么用過VC,在學校用過,那已經是好多年的事情了,現在重溫VC,不知道會是什么樣的感覺,年末了,大家都比較忙,我也是抽空,靜下心來嘗試的使用VC去開發,2個星期前在博客中發了一篇(http://www.gisall.com/html/63/151663-8220.html),那個是沒有界面的,也就是沒有用到MFC,訪問量還不錯,於是決定寫一個MFC的小例子,界面這塊,我一點都不擅長,習慣了C#中的做法,在C#中就是拖個按鈕,然后直接就在下面寫功能,界面是一個體力活,更是一個藝術活,像我這沒有藝術細胞的人,估計這輩子都做不了漂亮的界面,所以下面的小例子,大家也就不談論界面了,哈哈。現在言歸正傳,開始我們的旅程。
-
建立MFC工程
在這里可以選擇單文檔,也可以選擇基於對話框的,我選擇了單文檔,如下圖:
注意下面要選CFormView,默認的是CView,關於這兩個的區別看中間的這個詞語就知道了,如下圖:
-
添加類庫
在工程上右鍵,屬性找到VC目錄的欄目,在包含那個選項中添加
Engine安裝目錄下的com
SDK目錄下的CPPAPI
還有Common Files\ArcGIS\bin
因為我的目錄中有x86,添加后變成這個樣子了,如下圖:
在C/C++選項的預處理中添加:ESRI_WINDOWS,如下圖:
-
引入頭文件
在stdfx.h中引入ArcSDK.h 這個目錄文件,編譯的時候
會看到下面的錯誤(不要怕,名稱沖突而已)
兩種解決辦法:
-
重命名,找到相應的頭文件,在import指令后添加rename屬性(關於這些屬性大家可以自己搜索下)
#import "esrisystemui.olb" raw_interfaces_only raw_native_types no_namespace named_guids exclude( "OLE_HANDLE", "OLE_COLOR", "UINT_PTR" ) rename("ICommand", "esriICommand") rename("IProgressDialog", "esriIProgressDialog")
-
使用全名(命名空間+接口名稱)
#include "esrisystem.h"
#import "esrisystemui.olb" raw_interfaces_only raw_native_types named_guids exclude( "OLE_HANDLE", "OLE_COLOR", "UINT_PTR" )
其他的依次類推,注意如果重新命名之后,在程序中應使用新的名稱,不然還是會出錯。
-
綁定許可和初始化許可,綁定許可是10.0之后的必要操作
bool CMainFrame::AEinit(void)
{
#pragma region 綁定許可
IArcGISVersionPtr ipVer(__uuidof(VersionManager));
VARIANT_BOOL succeeded;
if (FAILED(ipVer->LoadVersion(esriArcGISEngine , L"10.1",&succeeded)))
return false;
#pragma endregion
#pragma region 初始化許可
IAoInitializePtr ipInit(CLSID_AoInitialize);
esriLicenseStatus status;
ipInit->Initialize(esriLicenseProductCodeEngine, &status);
if (status != esriLicenseCheckedOut)
AoExit(0);
return true;
#pragma endregion
}
-
如何顯示地圖
現在頭文件,初始化許可的事情都搞定了,但是地圖如何顯示?想象下C#是如何做的,拖一個地圖控件上去,然后將數據在這個地圖控件上顯示,但是這是VC,不是這么簡單,當然地圖控件肯定是ESRI提供,微軟不會提供一個容納地圖的控件吧?兩種方法:插入控件和直接生成相關的類,我們先用第二個方法,順便回一下MFC。
-
添加和地圖控件相關的MFC類
在工程上右鍵,添加類,選擇第一個,如下圖:
彈出一個向導,在可用的ActiveX控件中找到Esri的地圖控件,設置生成的類,如下圖:
在CMapMFCView類中添加頭文件,定義一個CMapControl2的變量,如下圖:
在類視圖中,右鍵該類,轉到"對話框",出現如下界面:
在上圖中拖一個按鈕上去,改一下屬性,並且添加單擊事件(使用類向導完成),然后添加下面的代碼(先不要管,我會在后面介紹,CWsClass類是我添加的用來獲取要素類的見后面)
void CMapMFCView::OnClickedBtn()
{
CRect r;
GetClientRect(&r);
CWsClass pWs;
BSTR bStringWS = SysAllocString(L"D:\\guest\\數據\\1");
BSTR bStrFcName = SysAllocString(L"1.shp");
IFeatureClassPtr ipFeatureClassTest=pWs.GetFeatureClass(bStringWS,bStrFcName);
SysFreeString(bStrFcName);
SysFreeString(bStringWS);
IFeatureLayer *ipFeatureLayerX;
// 查看注冊表或者tlh文件{E663A651-8AAD-11D0-BEC7-00805F7C4268} 完完全全通過COM自己創建對象
HRESULT hr1=::CoCreateInstance(CLSID_FeatureLayer,NULL,CLSCTX_ALL,IID_IFeatureLayer,(void**)&ipFeatureLayerX);
if(FAILED(hr1))
return;
//賦值
ipFeatureLayerX->putref_FeatureClass(ipFeatureClassTest);
//這句類似Windows的用類來創建窗體(MapControl控件)
m_MapControl.Create(L"Test",WS_CHILD|WS_VISIBLE,r,this,10000);
//獲取指針
IMapControl2Ptr m_ipMapControl=m_MapControl.GetControlUnknown();
IDataset *ipDsetTest;
HRESULT hr2=ipFeatureLayerX->QueryInterface(__uuidof(IDataset),(void**)&ipDsetTest);
if(FAILED(hr2))
return;
m_ipMapControl->AddLayer(ipFeatureLayerX,0);
m_MapControl.put_BackColor(16777215);
//手動釋放
ipFeatureLayerX->Release();
ipDsetTest->Release();
////ATL方式也是智能指針,我們不需要手動釋放
//CComPtr<IFeatureLayer> ipFtLayer;
//ipFtLayer.CoCreateInstance(CLSID_FeatureLayer);
//
//ipFtLayer->putref_FeatureClass(ipFeatureClassTest);
//m_MapControl.Create(L"Test",WS_CHILD|WS_VISIBLE,r,this,10000);
////QI
//IDataset *ipDset;
//ipFtLayer.QueryInterface(&ipDset);
//BSTR bSName=SysAllocString(L"");
//ipDset->get_Name(&bSName);
//ipFtLayer->put_Name(bSName);
//m_MapControl.put_BackColor(16777215);
//m_MapControl.AddLayer(ipFtLayer,0);
//
////第三種方式,這個比智能指針有優勢,自動QueryInterface
/*IFeatureLayerPtr ipFeatureLayer;
HRESULT hr = ipFeatureLayer.CreateInstance(CLSID_FeatureLayer);
ipFeatureLayer->putref_FeatureClass(ipFeatureClassTest);
m_MapControl.Create(L"Test",WS_CHILD|WS_VISIBLE,r,this,10000);
m_MapControl.put_BackColor(16777215);
m_MapControl.AddLayer(ipFeatureLayer,0);*/
////第四種方式
/*IFeatureLayerPtr ipFeatureLayer(CLSID_FeatureLayer);
ipFeatureLayer->putref_FeatureClass(ipFeatureClassTest);
IDatasetPtr ipDs(ipFeatureClassTest);
m_MapControl.Create(L"Test",WS_CHILD|WS_VISIBLE,r,this,10000);
m_MapControl.put_BackColor(16777215);
BSTR bSName=SysAllocString(L"");;
ipDs->get_Name(&bSName);
ipFeatureLayer->put_Name(bSName);
m_MapControl.AddLayer(ipFeatureLayer,0);*/
//m_MapControl.AddShapeFile(L"D:\\guest\\數據\\1",L"1.shp");
}
添加一個獲取要素類的類,如下圖:
創建完類之后,添加一個獲取要素類的函數(該函數的代碼,等看完后面的代碼說明之后應該就一目了然)
IFeatureClassPtr CWsClass::GetFeatureClass(BSTR sWorkspacePath,BSTR sFileName)
{
CComPtr<IWorkspaceFactory> ipWorkspaceFactory;
ipWorkspaceFactory.CoCreateInstance(CLSID_ShapefileWorkspaceFactory );
IWorkspace * m_workspace;
ipWorkspaceFactory->OpenFromFile(sWorkspacePath,NULL,&m_workspace);
IFeatureClassPtr ipFeatureClasses;
if(m_workspace!=NULL)
{
IFeatureWorkspacePtr ipFeatWorksapce(m_workspace);
HRESULT hr= ipFeatWorksapce->OpenFeatureClass(sFileName,&ipFeatureClasses);
if(FAILED(hr))
return NULL;
}
return ipFeatureClasses;
}
運行后可以看到如下效果(已經說了,不要嘲笑界面)
-
代碼解釋
需要對Windows編程和MFC有了解,在這里我只介紹和ArcGIS Engine相關的.
-
如何實例化對象
-
最原始做法(我自己這樣稱呼,學習COM的時候就是這種方式吧)
-
ArcGIS Engine 的組件庫都是基於COM技術的,在COM中我們都是通過接口來完成任務的,但是在使用接口之前,必須要知道這個接口指向的是那個對象,也就是說我們必須實例化對象,然后將對象的返回地址賦給一個接口變量,在C#中這種關系似乎很清楚,如下:
IRasterGeometryProc pRasterGProc = new RasterGeometryProcClass();
之后我們就可以通過pRasterGProc相關操作了,但是我們現在用的是C++,上面這個是不能用了,如果看過《COM本質論》或者其他和COM相關的書籍的人可能知道在COM中是通過CoCreateInstance函數來獲取一個對象的指針變量的,具體的代碼如下:
// 查看注冊表或者tlh文件{E663A651-8AAD-11D0-BEC7-00805F7C4268} 完完全全通過COM自己創建對象
HRESULT hr1=::CoCreateInstance(CLSID_FeatureLayer,NULL,CLSCTX_ALL,IID_IFeatureLayer,(void**)&ipFeatureLayerX);
if(FAILED(hr1))
return;
//賦值
ipFeatureLayerX->putref_FeatureClass(ipFeatureClassTest);
在COM中,因為一個類可以實現多個接口,每個接口只可以訪問自己定義的方法,如果要使用定義在別的接口中的方法呢?我們自然而然的想到了將這個接口"切換"過去,通俗的講就是這樣,只不過專業術語要做QueryInterface(接口查詢/訪問,英語的理解就行了,不糾結這個翻譯了),在C#中我們使用一個as就可以解決問題,如下(IRaster QI到IRasterProps 上):
IRaster pRaster = pRasterLayer.Raster;
IRasterProps pRasterPro = pRaster as IRasterProps;
C++中的做法和這個不一樣,但是目的一樣,都是為了"切換"到另外一個接口上,因為要素類實現了IDataset中的方法,那么肯定是可以QI到這個上面去的,下面是做法:
IDataset *ipDsetTest;
HRESULT hr2=ipFeatureLayerX->QueryInterface(__uuidof(IDataset),(void**)&ipDsetTest);
if(FAILED(hr2))
return;
我們使用了接口,如果在不用的時候一定要自己給釋放掉,如下面所示:
ipFeatureLayerX->Release();
ipDsetTest->Release();
現在要素類有了,要素圖層有了,我們將這兩個關聯起來,一起放到地圖控件中去,但是我們的地圖控件在哪里,如果習慣了C#中的那種直接將控件拖到窗體上的做法,乍一看一頭霧水,別急,待會兒就雨過天晴了。
Esri在在VC中也提供了我們所謂的控件,我們通常稱之為ActiveX控件,在這里我沒有插入和拖放,而是直接用類來創建,還記得我剛才插入類的時候,選擇了ActiveX控件中的MFC類,又接着選擇了MapControl,然后我們的工程中就多了一個類,這個類其實跟一般的MFC類沒多大區別,只是這個類里包含和和地圖相關的東西,在這里說一下MFC類,MFC類其實就是一個C++類,只是這個類里面封裝了Windows的句柄(窗口,菜單等),而ActiveX控件本質上也算是一個窗口吧。我們要得到一個窗口句柄,按照MFC的邏輯:首先實例化一個類,然后用類的Create函數構造這個句柄(MFC在內部已經做了處理):
//這句類似Windows的用類來創建窗體(MapControl控件)
m_MapControl.Create(L"Test",WS_CHILD|WS_VISIBLE,r,this,10000);
//獲取指針
IMapControl2Ptr m_ipMapControl=m_MapControl.GetControlUnknown();
m_ipMapControl->AddLayer(ipFeatureLayerX,0);
m_MapControl.put_BackColor(16777215);
在這里我們可以通過類來操作,也可以通過接口來操作,這里我用m_ipMapControl添加了圖層,下面的幾個代碼中用m_MapControl來操作,請注意這兩個的不同。
如果使用上面的方式,我們要不斷的使用CoCreateInstance(),QueryInterface(),Release()等方法,對我們來說有太多的不便,如果忘記了Release可能還會出問題,下來我們討論我注釋掉的幾種方法:
-
ATL方式(智能指針)
使用智能指針,我們不需要自己手動release等,這個會自動完成,但是對於QI,我們也需要寫,但是已經比上面的少了很多代碼了:
CComPtr<IFeatureLayer> ipFtLayer;
ipFtLayer.CoCreateInstance(CLSID_FeatureLayer);
ipFtLayer->putref_FeatureClass(ipFeatureClassTest);
m_MapControl.Create(L"Test",WS_CHILD|WS_VISIBLE,r,this,10000);
//QI
IDataset *ipDset;
ipFtLayer.QueryInterface(&ipDset);
BSTR bSName=SysAllocString(L"");
ipDset->get_Name(&bSName);
ipFtLayer->put_Name(bSName);
m_MapControl.put_BackColor(16777215);
m_MapControl.AddLayer(ipFtLayer,0);
當我們import ArcGIS Enine的COM組件是,編譯器自己會使用_com_ptr_t幫我們生成,如下圖:
關於中方式和ATL的區別,我在網上查了下,看到下面幾點:
在用VC開發應用程序時,有兩個引用計數類可供我們使用。_com_ptr_t與CComPtr,它們都能很好的幫助我們解決引用計數處理。但這兩個類還是有一點小小的區別,有的時候這一點區別也是致命的,因此我們必須清楚它們的差別。下面我羅列了它們之間的差別:
1. CComPtr的&運算符不會釋放原指針,而_com_ptr_t會釋放原指針。
2. CComPtr對AddRef與Release做了限制,也就是不充許調用這兩個方法,而_com_ptr_t並沒有限制。
3. CComPtr只能接受模版參數指定的指針,_com_ptr_t可以接受任何接口指針,並自動調用QueryInterface得到模板參數指定的指針類型。
這種方式又可以有兩種方式來實例化對象,通過CreateInstance函數和直接用在聲明的時候用CLSID:
IFeatureLayerPtr ipFeatureLayer;
HRESULT hr = ipFeatureLayer.CreateInstance(CLSID_FeatureLayer);
和:
IFeatureLayerPtr ipFeatureLayer(CLSID_FeatureLayer);
ipFeatureLayer->putref_FeatureClass(ipFeatureClassTest);
我也不知道這種方式如何命名,但是這種比ATL的那種方式更智能,這體現在QI的時候也簡單了很多:
IFeatureLayerPtr ipFeatureLayer(CLSID_FeatureLayer);
ipFeatureLayer->putref_FeatureClass(ipFeatureClassTest);
IDatasetPtr ipDs(ipFeatureClassTest);//這句話就QI過去了
m_MapControl.Create(L"Test",WS_CHILD|WS_VISIBLE,r,this,10000);
m_MapControl.put_BackColor(16777215);
BSTR bSName=SysAllocString(L"");;
ipDs->get_Name(&bSName);
ipFeatureLayer->put_Name(bSName);
m_MapControl.AddLayer(ipFeatureLayer,0);