-
使用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);