简介:MapWinGIS是一款采用C++编写、以OCX/ActiveX控件形式发布的开源GIS组件,面向VC++与Delphi开发者,用于在Windows桌面应用中快速集成地图显示、矢量与栅格数据处理、地理分析等功能,也适合需要调用在线地图服务的MFC项目。该版本为v5.2.4.0,RAR压缩包约302.62MB,包含完整源代码、第三方依赖库及可编译的预构建文件:master目录对应项目主分支源码,Licenses目录列出各依赖库的许可协议,bin目录存放编译产物,结构清晰,便于开发者研究内部实现并进行深度定制。对于不熟悉GIS底层原理的开发者,可直接在VC++或Delphi工程中注册并调用该OCX控件,快速构建地图界面;在此基础上还可结合MFC框架打造专业桌面GIS应用,并扩展接入百度、高德等常见栅格图层。目前已有741人学习下载,适合需要快速实现地图功能的C++与Delphi开发者参考学习,也可作为深入理解OCX控件与GIS集成的实用范本。
1. MapWinGIS是什么东西,凭什么能通吃VC++和Delphi
1.1 核心能力和技术底子
我在VC++和Delphi里做GIS程序做了不少年头,最磨人的不是业务逻辑,而是地图窗口。早期试过MapX、MapObjects,好用是好用,商业授权一续费就肉疼;后来转开源,用了几年MapWinGIS(OCX)之后,基本稳定在它上面了。直接把踩过的坑和能用的代码整理出来,分享给正打算在VC++或Delphi里接地图控件的朋友。
MapWinGIS是用C++写的ActiveX地图控件,本质上就是一个OCX,核心数据读写建立在GDAL/OGR之上。这意味着它天然支持Shapefile、GeoTIFF、GeoJSON、ECW这些常见格式,不用为每个格式单独写解析器。对做桌面工具的人来说,界面不华丽没关系,能打开、能画出来、能点选查询,这三件事最重要。MapWinGIS这三点都能做到,该有的基础空间分析功能,比如缓冲区、面积计算、相交叠加,也都有封装。
很多人问过为什么不用C++静态库或者.NET控件,偏偏选OCX。原因很简单:ActiveX在Windows生态里兼容性最好。VC++的MFC对话框可以直接从工具箱拖进去,Delphi通过Import ActiveX Control生成包装单元,也能当原生控件用。就算手里维护的是十几年前的老项目,只要编译环境能注册COM组件,就能把它接进来。
1.2 和商业GIS控件摆在一起怎么选
总有人拿MapWinGIS和MapX、MapObjects、ArcEngine比。我的看法是,授权预算充足、要做复杂制图和拓扑编辑,商业控件确实省心;但如果是内部数据浏览工具、小型GIS工具、或者要部署给非技术用户,MapWinGIS的免费和轻量就是实打实的好处。安装包通常只有几十MB,程序目录里带一套dll就能跑,不必让客户先装庞大的运行时。
当然,它也有明显的短板。UI交互比较朴素,没有成熟的符号库和地图排版功能,对Undo/Redo支持也比较弱。如果你要做的是类似桌面GIS平台级产品,那得评估清楚,MapWinGIS只适合做基础显示和查询,复杂交互部分要么自己补,要么另外引入更重的引擎。我的经验是,先把需求拆成“显示、查询、分析、编辑”四档,MapWinGIS承包前两档完全没问题。
实际项目中,我用它做过一个管理地块的辅助工具,功能就是加载地块shp,叠影像底图,点击地块看属性,有时导出一张图放到Word报告里。整套工具用Delphi搭界面,核心地图操作全部走MapWinGIS,从开发到交付不到一周,后续也基本没出过稳定性问题。
2. 开始前的关键一步:下载、注册OCX,以及两种开发语言的接入方式
2.1 下载和依赖库准备
使用MapWinGIS最直接的方式,是去官方渠道下载编译好的release包。解压后会看到MapWinGIS.ocx,还有gdal、proj等一堆dll,以及gdalplugins目录。这里有两个常见坑:一是不要只复制ocx就到处用,它运行时依赖周边dll;二是注意32位和64位版本要和你的编译目标保持一致。我在32位Delphi程序里试过加载64位版本的ocx,注册成功但运行时创建控件就崩溃,最后统一换成32位版本才正常。
另外,部署机器上需要装对应版本的Visual C++ Redistributable。如果你在客户现场遇到“无法启动此程序,因为计算机中丢失VCRUNTIME140.dll”这类报错,先别怀疑程序写错了,大概率就是缺运行库。装上VC++运行库就能解决,这一步太容易被忽略,尤其是做绿色免安装工具的时候。
2.2 正确注册MapWinGIS.ocx并处理regsvr32报错
注册命令本身很简单,管理员身份打开命令行,进入ocx所在目录,执行:
regsvr32 MapWinGIS.ocx路径带空格时最好用引号包起来。成功时系统会提示“DllRegisterServer succeeded”,到这一步就说明控件已经在系统里登记好了,可以被外部程序实例化。
最烦的是遇到“unable to register the dll/ocx regsvr32”这类弹窗。常见原因有三个:一是缺少VC++运行库,或者系统库损坏;二是ocx依赖的dll不在当前目录,也不在系统搜索路径;三是32位/64位不匹配。排查时可以先用对应架构的regsvr32重新注册,比如64位系统注册32位dll,需要从SysWOW64目录调用regsvr32。再确认依赖dll与ocx同目录,最后用Process Monitor看加载失败的具体dll,基本都能定位。
2.3 在VC++工程里接入MapWinGIS控件
VC++里最简单的接入方式是在MFC对话框设计器中,右键对话框空白处,选择“插入ActiveX控件”,在列表里找到MapWinGIS相关的Map Control,点确定后它会自动生成包装类。之后在代码里就可以直接操作m_map对象的属性方法,这种方式对MFC项目最省事。
如果是非MFC的Win32项目,或者想动态创建,可以用#import指令:
#import "MapWinGIS.ocx" named_guids raw_interfaces_only using namespace MapWinGIS; ::CoInitialize(NULL); IMapPtr pMap = NULL; pMap.CreateInstance(CLSID_Map);使用这种方式,记得在程序退出时调用CoUninitialize,并且Unicode字符集和多字节字符集要保持一致,否则BSTR转换容易出乱码。实测下来,MFC对话框里插入ActiveX控件的方式维护成本最低,代码处理也最直观,适合绝大多数桌面工具场景。
2.4 在Delphi里安装MapWinGIS组件包
Delphi接入同样不复杂。菜单Component → Import Component → Import ActiveX Control → 从已注册列表中选择MapWinGIS,把生成的单元安装到一个包中,之后它就会出现在组件面板里。直接在窗体上拖一个MapWinGIS,设置好Align属性,就能开始写代码。
如果不想安装到组件面板,也可以导入类型库后手动创建,这种方式更适合封装成公共类的情况:
uses MapWinGIS; var Map: TMapWinGIS; begin Map := TMapWinGIS.Create(nil); Map.Parent := Panel1; Map.Align := alClient; end;不同Delphi版本在导入ActiveX时偶尔会有接口定义兼容问题,遇到编译报错时,先用TLibImp工具重新生成_TLB单元。Delphi 2007以后一般不会有大问题,但Delphi 11等新版导入老控件时,注意检查GetActiveOleObject返回值类型有没有被包装成IUnknown,否则调用方法时容易卡在类型转换上。
3. 核心对象拆解:Map、Shapefile、Image和坐标系
3.1 分层结构和加载函数
MapWinGIS把地图结构搞得很简单:一个Map相当于画布,里面可以挂多个Layer,每个Layer是一类数据,比如一个Shapefile、一张影像、一个Grid。没有ArcGIS里那种MapDocument概念,这个极简模型对程序集成非常友好,写代码时脑子里不需要维护复杂的文档树。
加载Shapefile的代码很直接。VC++里先创建Shapefile对象,然后Open,再AddLayer:
IShapefilePtr sf; sf.CreateInstance(__uuidof(Shapefile)); if (sf->Open(_bstr_t("D:\\data\\city.shp"), NULL) == VARIANT_TRUE) { long layerHandle = m_map->AddLayer(sf, VARIANT_TRUE); }Delphi里写法几乎一样:
shp := CoShapefile.Create; if shp.Open('D:\data\city.shp', EmptyParam) = S_OK then Map.AddLayer(shp, True);这里要提醒:Open函数的第二个参数是密码或相关配置,通常传空。有些人习惯检查返回值,但MapWinGIS有些接口返回的是VARIANT_TRUE/False,有些返回类似S_OK的HRESULT,混用时很容易判断错。建议统一看文档中标注的返回值类型,再用正确常量判断,避免出现“明明加载成功却走了失败分支”这种诡异问题。
3.2 坐标和投影问题
不同图层显示不到一起的坑,多半出在坐标系上。MapWinGIS加载Shapefile时不会自动做重投影,它把数据当作“无坐标参考”直接画。底图是WGS84经纬度,边界数据是地方坐标系,叠在一起就错位。
解决办法有两个:数据预处理或动态转换。预处理最稳妥,起一个专业GIS工具把矢量数据的投影统一下,保存成新文件再加载,成本最低。动态转换可以用MapWinGIS的GeoProjection对象读取原投影,再调用Reproject相关API转成目标投影坐标,但这里涉及参数较多,建议只在数据源固定的内部工具中使用,避免每次展示都消耗大量计算。
3.3 常用的图层样式和属性查询
MapWinGIS默认会把Shapefile用一种随机颜色显示,要想好看一点,需要通过DefaultDrawingOptions设置填充色、边框、线条宽度、是否标注等。比如把面图层设置成半透明黄色:
sf->get_DefaultDrawingOptions()->FillColor = RGB(255, 200, 0); sf->get_DefaultDrawingOptions()->FillTransparency = 60;属性查询通常走Shapefile的Table对象,它对应dbf文件。拿到要素索引后,可以逐字段读取属性值。点选某个要素时,用Map.PixelToProj把鼠标位置转成地图坐标,再用Shapefile.ShapeAt或者手工遍历判断点落在哪个面内,这是点击选查功能的关键。
4. 手写一个最小可用的GIS查图工具(含核心代码)
4.1 界面布局和初始化
这个工具定位是“能打开shp和tif,能缩放、能点选、能导出图”。我在VC++里新建一个Dialog工程,放上地图控件和一个Open按钮,再放一个列表显示属性。Open按钮里弹出文件选择对话框,根据扩展名决定调用LoadShapefile还是LoadImage,加载后自动缩放到全图。
初始化时除了设置窗口标题和尺寸,还要调用Map.Clear清一次数据,避免调试过程中残留旧图层。如果希望地图填充整个客户区,需要在OnSize里处理控件大小;如果界面是固定尺寸,可以直接在资源编辑器里把控件摆放好,省去动态布局代码。
4.2 加载Shapefile和GeoTIFF影像
加载矢量数据,参考上面的代码即可。加载影像也类似:
IImagePtr img; img.CreateInstance(__uuidof(Image)); img->Open(_bstr_t("D:\\data\\remote.tif"), VARIANT_FALSE, VARIANT_FALSE, NULL); m_map->AddLayer(img, VARIANT_TRUE);这里Open的第二个参数表示是否加载到内存,一般传false,让底层GDAL惰性读块,减少内存占用;第三个参数是构建缩略图相关选项,先传false。大tif加载慢的话,可以先用命令行gdaladdo生成金字塔文件,让影像按比例尺显示不同精度的切片,实测能明显减少拖拽时的卡顿。
4.3 缩放漫游和点击选查
Map控件本身有鼠标模式,在VC++里设置MouseMode属性,能实现内置的放大、缩小、平移,不需要自己处理鼠标消息。点击选中要素则是在事件处理函数里做坐标反算和要素检索:
procedure TForm1.MapMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var pt: ProjPoint; idx: Integer; begin if not FShpLoaded then Exit; pt := Map.PixelToProj(X, Y); idx := FShp.ShapeAt(pt.x, pt.y, 0.01); if idx >= 0 then Memo1.Lines.Add('当前选中要素索引:' + IntToStr(idx) + ',名称:' + FShp.TableValue(idx, 1)); end;ShapeAt的第三参数是搜索容差,单位是当前地图的坐标单位。经纬度数据里写0.01约等于一公里,如果只想精确点击,写小一点,比如0.0001。这里很容易忽略,导致点击时误选中远处的要素,实际使用时要根据数据精度调节。
4.4 结果导出一张图片
导出功能可以调用Map的SaveMap方法。VC++示例:
m_map->SaveMap(_bstr_t("C:\\output\\map.png"), (unsigned int)1920, (unsigned int)1080, RGB(255,255,255));如果调用ExportMap,则可以获得更灵活的输出对象,适合嵌入到自己的打印模块里。导出图片和屏幕显示不同,可以设置比窗口更高的分辨率,适合做报告配图。要注意导出前先把地图居中到目标区域,否则导出的内容范围和用户看到的不一样。
5. 跨语言使用过程中踩过的那些坑(速查表)
5.1 按现象定位问题的排查表
我把常见的几个问题整理成了一张表,遇到类似报错可以先按表排查:
| 现象 | 原因 | 建议 |
|---|---|---|
| regsvr32注册失败 | 缺VC++运行库或dll依赖不全 | 装对应架构redistributable,确认gdal等dll与ocx同目录 |
| 创建控件时崩溃 | ocx位数与工程位数不一致 | 统一使用32位或64位版本 |
| Open返回失败 | 中文路径、权限或文件被占用 | 使用纯英文路径,以管理员身份运行,关闭占用文件的程序 |
| Delphi接口方法调用报类型错 | 导入的TLB版本过旧 | 用TLibImp重新导入,更新包装单元 |
| 大数据量缩放卡顿 | 图层绘制精度过高 | 关闭标注,开启动态可见性,适当抽稀要素 |
5.2 Delphi数据集打开状态冲突
搜索热词里有一个“cannot perform this operation on an open dataset”,我在用MapWinGIS做属性表联动时也遇到过类似怪象:用TADOQuery打开dbf属性表后,再操作Shapefile.Table,偶尔报错。后来定位到根本原因是同一个dbf被两个连接重复打开,或者数据访问组件没有关闭就再次打开。解法是先检查ActiveState,把要操作的数据集先Close再Open。这个错误和MapWinGIS本身关系不大,但因为是“地图属性表”场景,容易让人误判。
另一个容易被忽略的点是,MapWinGIS的COM对象不是线程安全的。我在Delphi多线程程序里后台线程执行缓冲区分析,主线程同时操作Map,结果时不时崩溃。建议所有MapWinGIS对象都在主线程里调度,长时间计算放到独立线程并只操作Shapefile对象,计算完成后通过同步机制交回主线程。
5.3 中文路径导致数据打不开
GDAL在Windows下默认文件名为UTF-8,但很多国内用户的数据路径是中文,旧版GDAL可能解析不了。我在自己程序里会做一轮预处理:如果加载失败,就提示用户复制数据到纯英文路径;更自动化的方案是程序内部将文件复制到临时目录再加载。这个方法比较笨,但有效。也可以在打开前设置gdal.SetConfigOption('GDAL_FILENAME_IS_UTF8', 'CN'),但不同版本行为有差异,最好实测。
5.4 大数据量的显示性能怎么优化
如果Shapefile里有几十万个多边形,MapWinGIS加载后内存占用不算大,但缩放平移会掉帧。我的经验是:不要直接提高绘制精度,优先做好数据抽稀;数据入库后按比例尺范围控制显示层级;只显示当前视图内的要素。MapWinGIS提供了VisibilityExpression和DynamicVisibility,可以在一定缩放级别隐藏部分图层或字段。再把不需要标注的图层关闭标注,性能提升非常明显。
如果需求是做全国级别的数据,一个shp图层承载还是太重,建议切成小区域瓦片或者转成更贴合场景的缓存格式,MapWinGIS直接读海量shp会比较吃力。这里没有银弹,只能按场景取舍。
最后说点个人体会。我在做这个“Mini GIS查看器”时,其实没有用太多奇技淫巧,MapWinGIS的稳定性和API设计超出我对开源控件的预期。尤其对于时间紧、预算少的桌面工具项目,它真的能帮大忙。如果你在VC++或Delphi里接它,有一件事一定别偷懒:把ocx和gdal相关dll作为安装包的一部分一起发布,并写一个判断是否需要注册的安装脚本,否则部署到客户现场时很容易在这里折腾半天。这算是我最实在的一条建议。
本文还有配套的精品资源,点击获取