
简介这是一套面向Visual C开发者的MapX GIS控件入门示例适合需要在MFC程序中集成地图显示、图层管理与数据查询的读者参考。压缩包提供完整的SerialSample工程围绕MapX控件在VC环境下的初始化、图层增删与可见性控制、地图缩放平移、要素查询及事件响应展开并涉及AddLayer/RemoveLayer、图层顺序调整与可见性切换等常用接口可直接打开dsw工程编译运行。资源共30个文件约60KB以11个h头文件和10个cpp实现文件为主体附带图标、位图、工程配置dsp/dsw/rc及说明txt结构清晰便于边读源码边理解MFC中调用MapX的完整流程。当前已有127人学习可作为VC中MapX图层操作的快速上手范例能帮助开发者省去配置控件和摸索接口的时间直接对照代码实现GIS基础功能。1. 为什么 VC 里调 MapX 比想象中顺利直接拆能编译的工程很多人找我调 VC 工程时一听要用 MapX 控件做地理图层第一反应都是去翻 SDK 文档。我的建议一直很直接不用急着一页页读文档先找一份能完整编译的工程把它拆开图层添加、删除、排序、可见性这几个核心操作半天就能全摸透。这份「vcmapx例子.rar」就是一个现成的 MFC 工程名字叫 SerialSample文件里把 CMapX 控件的创建、加载地图、图层管理、交互事件都串了起来。它适合两种人刚接触 MapX 想快速上手的项目新人以及手里有老 MFC 代码要做 GIS 功能收尾的维护工程师。注意这工程是 Vc6 时代的产物拿到手的第一件事不是读代码而是先把环境对齐。2. 搭建环境与读懂工程骨架装好 MapX再谈 VS2008 转换工程拿到压缩包很多人直接点开 .dsp 就编译结果报错一片。这不能怪项目只能怪环境没对齐。MapX 是 ActiveX 控件VC 要用的那一层封装类依赖的是已经注册到系统里的 OCX而不是仅仅解压出来的一堆源文件。2.1 先把 MapX 控件装对32 位 OCX 与注册路径常见做法是先安装 MapInfo MapX 5.0在安装目录里能找到类似 MAPX50.OCX 的文件。如果安装包已经不可用你也可以手动注册从别人工程里拷贝的 OCX但千万别从 64 位目录拷贝。MapX 控件是 32 位组件在 64 位 Windows 上必须走 32 位注册路径否则 VC 工程运行时找不到控件。# 以管理员身份打开命令提示符执行 32 位控件注册 regsvr32 C:\Program Files (x86)\MapInfo\MapX 5.0\MAPX50.OCX这里的 regsvr32 是 Windows 自带的组件注册工具作用是把 OCX 的类标识符写进注册表。如果你的 MapX 安装目录不在默认路径就把引号里的路径改成实际地址。注册成功后系统会弹出一个提示框如果没弹用 regsvr32 /s 静默注册然后去注册表编辑器里搜索 MapX 相关的 CLSID 来验证。这一步没过后面工程编译得再好也是白搭运行时会直接弹出 ActiveX 控件无法创建的错误。2.2 .dsp 和 .dsw 的关系VC6 工程怎么转移到 VS2008SerialSample 里面有两个关键文件SerialSample.dsp 和 SerialSample.dsw。先说结论.dsp 是工程文件里面保存了源文件列表和编译选项.dsw 是工作区文件管理多个 .dsp 的集合。如果你用 Visual Studio 2008 打开 .dsp会弹出一个“Visual Studio 转换向导”。转换时我一般建议选择“不备份”因为这种老工程转完基本都是直接改原文件备份一份旧的 .dsp 意义不大。转换完成后你会看到解决方案平台自动变成 Win32。这时不要急着编译先去项目属性里检查两处字符集是否设置为“使用 Unicode 字符集”以及 C/C 代码生成里的运行时库是否为“多线程 DLL (/MD)”。这两项是 MapX 工程编译最常见的翻车点后面避坑章节会细讲。2.3 工程骨架读法先从 Doc 和 View 的分工看起SerialSample 是一套标准的 MFC 文档视图结构SerialSampleDoc.cpp 负责数据SerialSampleView.cpp 负责地图显示。这是 MFC 骨灰级惯例不需要做额外设计直接沿用。工程里还有几个不能忽略的文件Mapx.cpp 和 Mapx.h 是 MapX 控件类封装的实现IHelper.cpp 是功能辅助类AddThemeDialog.cpp 负责主题图层参数设置ChooseListDialog.cpp 负责图层选择列表。看这个文件组成就能猜到这个例子并不只是简单显示一张地图它把图层管理的交互 UI 也做好了。看代码顺序我建议先从 SerialSampleView.cpp 读起因为地图控件是在视图创建时被实例化的这是整个例子的入口。MainFrm.cpp 虽然负责主窗口框架但地图控件不在它的职责范围内别在那里浪费太多时间。3. 图层管理核心操作CMapX 的添加、删除、排序与可见性这一章是整篇笔记的干货所在。SerialSample 里所有与图层有关的操作本质上都围绕着 CMapX 的 Layers 集合展开。如果你只记住一个对象模型那就记这个CMapX 是地图控件本身CMapXLayers 是它的图层集合CMapXLayer 是集合里的单个图层。3.1 CMapX 对象在视图里的创建方式SerialSample 的视图类里会声明一个 CMapX 类型的成员变量然后在视图创建时调用 Create 方法把这个 ActiveX 控件挂到窗口上。这是 MFC 集成 ActiveX 的标准做法跟你拖一个按钮控件到对话框上是两套逻辑。// SerialSampleView.h 中的成员声明 class CSerialSampleView : public CView { public: CMapX m_MapX; // MapX 控件包装对象 UINT m_nMapId; // 控件 ID用于事件映射 };// SerialSampleView.cpp 的 OnCreate 中创建地图控件 int CSerialSampleView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) -1) return -1; // 创建一个子窗口位置和大小先用默认值后续在 OnSize 里调整 if (!m_MapX.Create(NULL, WS_CHILD | WS_VISIBLE, CRect(0, 0, 100, 100), this, IDC_MAPX1)) { TRACE(MapX 控件创建失败\n); return -1; } // 加载地图集这是显示地图的必经步骤 m_MapX.SetGeoSet(_T(D:\\MapData\\demo.gst)); return 0; }这段代码里Create 的第五个参数this是父窗口指针第六个参数IDC_MAPX1是控件 ID。ID 在资源头文件里定义占一个数字位用于后来的事件映射。SetGeoSet 的作用是加载一个地图集文件.gst 内部记录了多个图层的路径和显示参数类似把图层配置打包成一个入口。假如不调用 SetGeoSet控件窗口就是一张灰白空纸连地图底图都没有。很多新手在这里容易犯一个错在对话框工程里用 ClassWizard 导入 MapX 控件生成的封装类直接把 CMapX 当成对话框的普通子控件在 DoDataExchange 里绑定。那样做不是不行但在 CView 工程里想通过 DDX 绑定地图控件是走不通的因为视图类不是对话框模板没有资源模板承载控件。SerialSample 的做法才是视图工程里通用的一种用 Create 动态创建把控件实例和视图类绑定在一起。3.2 添加与删除图层参数到底是文件路径还是图层对象MapX 添加图层的方法签名很灵活既可以把图层文件路径传给 Layers 集合也可以把已经构造好的图层对象传进去。不同传参方式影响的是底层数据源的连接方式使用逻辑没有太大差别。// 添加一个 Tab 图层并控制其可见性 CMapXLayers layers m_MapX.GetLayers(); COleVariant varTabPath(_T(D:\\MapData\\city.tab)); CMapXLayer layer layers.Add(varTabPath); layer.SetVisible(FALSE); // 默认隐藏 layer.SetSelectable(TRUE); // 可选则设为真允许鼠标点选// 删除图层按索引或按图层对象 CMapXLayers layers m_MapX.GetLayers(); layers.Remove(0); // 移除集合里的第一个图层第一段代码里COleVariant 是 MFC 对 COM 变体类型的封装MapX 的 Add 方法是 COM 接口参数必须是 VARIANT 类型所以要把路径字符串包装进去。Add 成功后返回一个 CMapXLayer可以用它继续设置图层属性。SetVisible(FALSE) 把图层隐藏起来但数据源仍然保持着连接比移除再重新加载要快得多。SetSelectable 控制在交互工具状态下图层是否响应鼠标点击这在后面做地图查询的时候特别有用。第二段代码的 Remove 是集合的删除方法。注意这里删除的是 Layers 集合中的一项不是从磁盘删除图层文件所以不需要有任何心理负担删掉后随时可以用同一路径重新添加回来。3.3 图层顺序调整Move 方法与 Z 轴堆叠的逻辑图层的显示顺序就是图层的先后次序底层图层画在地图底层上层图层覆盖在它上面。在 MapX 的 COM 接口里图层的排序是通过 Layers 集合的 Move 接口来调整的SerialSample 里如果你看到了类似 Raise 或 Lower 的封装方法其内部也是在调整图层的 z 序号。// 把一个图层移动到指定索引位置 CMapXLayers layers m_MapX.GetLayers(); CMapXLayer layer layers.Item(0); // 获取第一个图层 layers.Move(layer, 2); // 移动到底层索引越大越靠近底层这里 Item(0) 和 Move 的组合是调整图层顺序时候最常用到的写法。需要特别注意索引值越小表示图层越靠上也就是画在最前面。很多人把它当成数组的下标理解以为索引越大越靠上结果调完顺序之后标注图层被其他面图层盖住了半天找不出原因。SerialSample 里面还有 AddThemeDialog 和 ChooseListDialog 两个对话框它们的功能从文件命名也能猜个大概。AddThemeDialog 是用来配置主题图层的典型做法是让用户选择一个字段然后把图层的渲染方式改为按字段值分级显示。这一层的主题操作没有 Surface 开发那么复杂MapX 已经把渲染逻辑封进了后台你只需要负责把字段名和目标图层传给主题对象。4. 交互事件与数据查询让地图图层能点击、能选中、能回传坐标MapX 作为一个 ActiveX 控件除了把地图画出来更重要的职责是把用户在地图上的点击、拖动、框选行为转成程序知道的数值。SerialSampleView.cpp 里肯定写有事件映射代码这块内容往往是新手最容易忽略的。4.1 鼠标点击事件与经纬度换算MapX 默认提供 Click、DblClick、MouseDown 等事件MFC 工程中需要在消息映射里把事件绑定到消息号上。控件 ID 是 IDC_MAPX1事件消息名通常与 ID 组合成 ON_MAPX_CLICK 之类的宏具体宏名因 MapX 版本而异。afx_msg void OnMapClick(short Button, short Shift, long X, long Y); BEGIN_MESSAGE_MAP(CSerialSampleView, CView) ON_MAPX_CLICK(IDC_MAPX1, CSerialSampleView::OnMapClick) END_MESSAGE_MAP()这里 X 和 Y 是鼠标在控件窗口中的像素坐标单位不是米也不是经纬度。要想把像素坐标转换成真实的地理坐标得调用 MapX 的 ConvertCoord 方法。常见做法是在消息处理函数里写这么一段void CSerialSampleView::OnMapClick(short Button, short Shift, long X, long Y) { double dX 0.0, dY 0.0; // 把屏幕像素坐标转成地图坐标1 表示 mapX 到屏幕坐标转换 m_MapX.ConvertCoord(dX, dY, X, Y, 1); CString strMsg; strMsg.Format(_T(点击位置经度 %.6f纬度 %.6f), dX, dY); AfxMessageBox(strMsg); }ConvertCoord 的最后一个参数是转换方向1 通常表示从屏幕坐标转换到地图坐标0 是反向转换。如果你在代码里写成反向得到的就是一组莫名其妙的像素值在地图上对不上位置。4.2 查询图层数据SelectFeatures 用了之后记得释放地图上能点开查看属性信息靠的是图层图元的选中与查询。MapX 里选中某个图元后可以拿到它的 Feature 对象然后从 Feature 的 Fields 集合里读出所有属性值。CMapXFeature feature m_MapX.GetFeatureFactory().CreateFeature(CMapXFeatureType miFeatureTypeShape, ...);不过在实际工程中你很少手动构造 Feature更多是从用户点击产生的 Feature 集合里读取。SerialSample 里如果实现了要素查询流程应该是点击选中一个图元然后从选中集中获取 Feature 的 ID再根据 ID 去图层的数据源里查询对应记录。这里有个实际的坑Feature 对象在 MFC 里往往使用起来看似很方便但它内部封装的是 COM 接口指针用完之后需要释放。很多例子代码里直接声明一个 CMapXFeature 局部变量离开作用域后自动析构接口引用计数会由封装类负责维护这块问题不大。真正容易翻车的是在处理大量图元时反复调用 GetAllFeatures 这样的方法每次都返回一个新集合消耗的临时对象巨大内存涨得很快。建议是尽量通过选中集交互不要做全要素遍历。4.3 图层数据源边界矢量图层和栅格图层各管各的SerialSample 这个例子的 mapx 文件清单里并没出现 GDAL 或栅格图像处理的专用类所以在图层数据这一侧它默认面向的是 MapInfo 体系的 .tab 和 .gst。如果工程里需要加载卫星影像或 DEMMapX 也能做但是要额外引入栅格处理模块并且注意坐标系匹配。拿实际项目来说同一块区域矢量边界用经纬度坐标栅格影像用西安 80 投影坐标两者叠加时地图上会出现明显的偏移。SerialSample 里如果直接往 GeoSet 里塞不同坐标系的数据显示结果会给你上一课。最简单的处理办法是提前统一投影把栅格文件转换成与矢量一致的坐标系再加载进来。5. MapX 在 VC 里的常见问题与避坑记录这一章全是实操里积累下来的血泪经验。每一条我都按现象、原因、解决三个步骤写方便你对照排查。这里的每条经验不是从 SerialSample 项目本身推出来的但凡是老工程遇到这些现象八成就是下面几个原因。5.1 工程编译报错提示找不到 CMapX 类现象代码里明明写了 CMapX 成员变量编译器却说 CMapX 不是类或命名空间名称。原因MapX.h 没有被正确包含或者包含顺序不对。MapX.h 里有大量 COM 封装定义依赖 afxwin.h 等 MFC 基础头文件。如果你在 StdAfx.h 里加了 #include MapX.h但顺序放在 MFC 头文件之前编译器走到 CMapX 声明时一堆基类还没定义就会直接报错。解决把 #include MapX.h 放到 StdAfx.h 的末尾确保 MFC 框架头文件都已经加载。同时确认工程里确实有 MapX.h 这个文件SerialSample 压缩包里是有的如果你从网上下载的其他例程漏了头文件补上对应版本即可。5.2 编译通过但运行时提示 ActiveX 控件创建失败现象程序启动时地图区域一片空白调试输出窗口打印“MapX 控件创建失败”或者弹出“ActiveX control cannot be created”的对话框。原因最常见的是 OCX 没注册其次是系统缺少 Visual C 运行库。MapX 5.0 那时候生成的 ActiveX 控件依赖 MSVC 运行库而你如果用的是 Visual Studio 2008 编译工程运行时却装的是 VC 2015 或 2022 的库控件接口调用会失败。解决先按照第 2 章的 regsvr32 命令重新注册 OCX确认没有报错。再检查系统里是否装了微软官方 Visual C 2008 运行库也就是 vcredist_x86.exe。这里强调 x86因为地图控件是 32 位的即使操作系统是 64 位也必须装 32 位运行库。5.3 Debug 模式正常Release 模式运行时崩溃现象同一套代码Debug 配置编译通过且运行正常切到 Release 配置后加载地图或切换图层时程序直接异常退出。原因典型的配置不一致。MapX 控件在 Release 下崩溃十有八九是工程项目设置里运行时库选项不对默认 Release 是多线程 DLL (/MD)但某些老工程被改成了 /MT或者 Debug 用了 /MDd 而 Release 却用了 /MT造成运行时库的不匹配。解决打开项目属性找到 C/C 代码生成把 Runtime Library 统一设置为“多线程 DLL (/MD)”MFC 使用共享 DLL。另一个常见关联问题是 Release 下框架层忘记调用 AfxEnableControlContainer。在 MFC 工程里如果这个函数没有在 InitInstance 里被调用ActiveX 控件的容器支持就不完整Release 优化模式下会直接踩到空指针。5.4 地图中文地名显示成乱码现象图层数据里的中文标注在 MapX 控件里显示成问号或乱码英文地名正常。原因这是字符集问题。VC6 时代的工程默认是 ANSI 编码你的工程如果设置成了 UnicodeMapX 控件的字符串接口在内部使用的是 ANSI 语义两边不匹配中文就变成了乱码。解决SerialSample 这个例子本身就是 ANSI 工程编译时把项目字符集改成“未设置”或“使用多字节字符集”不乱码。如果你必须使用 Unicode那么对 MapX 的所有字符串参数都手动做 ANSI 转换这事没有捷径。比如加载图层时把 CString 转成 char 数组再传给 COleVariant不要让 MFC 自动把宽字符塞进 COM 变体里。5.5 窗口大小变化时地图区域留黑边或错位现象拖动主窗口边框时MapX 控件区域出现黑色边缘或者地图没有跟着窗口一起缩放。原因视图的 OnSize 消息里没有调整地图控件的尺寸。MapX 控件是独立子窗口父窗口变化后它不会自动感知你得手动把它的大小调整成客户区大小。解决在视图中重写 OnSize获取客户区矩形然后调用 m_MapX.MoveWindow 同步更新。void CSerialSampleView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); if (m_MapX.GetSafeHwnd() ! NULL) { m_MapX.MoveWindow(0, 0, cx, cy, TRUE); } }这里加了一个 GetSafeHwnd 的判断是为了防止窗口销毁阶段收到 OnSize 时去操作一个已经无效的控件句柄。这个判断在 VC 里是每个老工程师都会记住的细节不加的话程序关闭窗口时可能触发断言。从这个问题往后我养成了一个习惯凡是代码里出现子窗口自适应逻辑一律先判断句柄是否有效再调用 MoveWindow。6. 把 SerialSample 改造成自己的地图浏览框架到这一步SerialSample 已经能从编译到运行都跑通了接下来要做的不是继续翻文档而是把它改造成你项目里真正趁手的工具。我建议按一个固定的顺序去改造先验证事件链条再封装图层工具函数最后才是做界面美化。验证事件链条最好的方法是把地图点击的坐标换算写成一行调试输出然后在地图上点几个已知地物确认换算出的坐标与底图标注一致。这一步能同时验证 ConvertCoord 的方向参数和坐标系设置。如果坐标对不上先别急着往下改这是地图框架的地基。对比点法很简单找一个你确定经纬度的地物比如一座桥或一条路交叉口点击后看输出的坐标与参考值误差是否在合理范围内。接下来封装图层工具函数建议把添加图层、隐藏图层、删除图层、调整顺序这四件事分别封装成一个函数参数用文件路径和布尔值内部再去处理 COleVariant 和 CMapXLayer。这样做的好处是未来项目里其他模块只需要传路径进去不用关心 MapX 的具体 COM 接口。SerialSample 里的 IHelper.cpp 就是干这个角色的你在它基础上加一层自己的逻辑就行。最后一点经验是 SerialSample 本身没有做但在改造时必须补上的图层数据的坐标系一定要在加载时就统一别等到叠加显示时再用肉眼判断是否正确。我做过的上一个 GIS 项目中就因为一个 DEM 栅格的投影没对齐地图上几条等高线整体偏移了几百米排查了两天才发现是坐标系被默认成了 WGS84而矢量数据是 CGCS2000。从那以后我每次数据入库都强制走一遍统一的坐标系登记流程。MapX 不是新东西能踩的坑几乎都被人踩过了把 SerialSample 这个例子吃透你就等于拿到了前人铺好的路接下来只需要沿着路走遇到岔路再看坐标。希望这篇实战笔记能帮到你。本文还有配套的精品资源点击获取