简介这份资源是面向VC开发者的电子地图开发实践包围绕GeoExpert工具展开适合具备一定C基础、希望入门GIS与地图应用开发的学习者。内容涵盖地图缩放、区域选择、矢量与栅格数据加载显示等核心功能并延伸至空间查询、路径规划等GIS应用场景。压缩包共51个文件约4.01MB以cpp与h源码文件为主配合sbr、obj、pdb等编译调试中间文件以及ico、bmp图标位图资源和dsp、dsw工程文件构成一套可直接编译运行的完整VC工程。已有120人学习下载。通过研读源码与工程结构读者可理解地图瓦片分层管理、地理坐标与屏幕坐标转换、Shapefile与GeoTIFF等格式解析思路并借助示例代码快速搭建具备缩放、选择能力的地图应用为深入GIS开发打下基础。1. 从一份 VC6 老工程说起GeoExpert 到底能跑出什么效果如果你手头正好有一份GeoExpert.rar解压后看到.dsp、.dsw、MAPX.CPP、GeosetNameDlg.cpp这些文件第一反应大概率是这是个 VC6 时代的老古董。没错它就是一个典型的 MFC 单文档工程编译产物是GeoExpert.exe核心依赖是MAPX.H和MAPX.CPP这对文件。它解决的问题很具体——在 VC 环境里快速搭出一个带地图缩放、图层选择、Geoset 切换的电子地图桌面程序。适合谁适合还在维护 legacy GIS 桌面端、需要拿一个可编译的最小工程做二次开发底座的从业者。我见过太多人拿到这种包直接双击.dsw就翻车因为环境不对、依赖没配、Geoset 路径没设。这篇笔记就按“能编译、能跑起来、能改功能”这条线拆一遍。2. 工程结构与 MAPX 集成先搞清楚哪些文件能动、哪些不能动2.1 从文件清单反推工程骨架拿到一个 VC6 工程别急着打开 IDE。先看文件清单能读出很多信息。GeoExpert.dsp和GeoExpert.dsw是工程文件和工作区文件VC6 靠它们组织编译单元。GeoExpert.cpp是应用类实现MainFrm.cpp是主框架窗口GeoExpertDoc.cpp和GeoExpertView.cpp构成文档/视图结构。GeosetNameDlg.cpp是一个对话框从名字判断是用来输入或选择 Geoset 名称的。MAPX.CPP和MAPX.H是地图控件的封装层这是整个工程的核心。res目录下的toolbar1.bmp、Toolbar.bmp、GeoExpert.ico、GeoExpertDoc.ico是资源文件GeoExpert.rc是资源脚本。Debug目录里那一堆.obj、.sbr、.pdb、.ilk是编译中间产物说明这个包是编译过的但中间文件对重新编译没有正面作用反而可能因为路径不一致导致链接报错。常见做法是新建一个干净目录只保留源码和资源文件把Debug整个删掉把.ncb、.opt、.plg、.clw也删掉。.ncb是类浏览数据库.opt保存 IDE 布局.plg是编译日志.clw是 ClassWizard 信息这些都可以由 IDE 重新生成。保留它们只会让 VC6 打开时加载旧状态有时候会出现“类视图里看不到某个类”的玄学问题。提示删掉Debug目录前先确认ReadMe.txt里有没有写特殊编译步骤。有些老工程会在 ReadMe 里注明需要先注册某个 OCX 或设置特定环境变量。2.2 MAPX 封装层的调用逻辑MAPX.CPP和MAPX.H这对文件从命名看是对 MapX 控件的 C 封装。MapX 是当年很常见的一个地图组件提供 ActiveX 接口VC 里通过包装类来调用。工程里GeoExpertView.cpp会持有地图对象响应用户的缩放、平移、选择操作。我一般会先看MAPX.H里定义了哪些类和方法再对照GeoExpertView.cpp里的调用点。典型的调用链是这样的视图类在OnInitialUpdate里创建地图对象设置 Geoset 路径然后绑定鼠标事件。缩放通过设置Zoom属性实现地图选择通过Selection对象操作。// GeoExpertView.cpp 中典型的地图初始化片段示意 void CGeoExpertView::OnInitialUpdate() { CView::OnInitialUpdate(); // 创建地图对象m_ctrlMap 是 MAPX.H 中定义的封装类实例 if (!m_ctrlMap.Create(NULL, WS_VISIBLE | WS_CHILD, CRect(0,0,100,100), this, IDC_MAP)) { AfxMessageBox(_T(地图控件创建失败检查 MapX 是否已注册)); return; } // 设置 Geoset 路径指向 .gst 文件所在目录 m_ctrlMap.SetGeoSet(_T(C:\\MapData\\world.gst)); // 初始缩放级别和中心点 m_ctrlMap.SetZoom(1000); // 缩放值单位与地图坐标系相关 m_ctrlMap.SetCenterX(116.4); // 中心点经度 m_ctrlMap.SetCenterY(39.9); // 中心点纬度 }这段代码里几个参数需要特别注意。SetGeoSet的路径必须是绝对路径相对路径在 VC6 运行时的工作目录不确定很容易加载失败。SetZoom的值不是“级别”而是地图单位值越大视野越窄。SetCenterX和SetCenterY用的是地图坐标系如果 Geoset 用的是经纬度坐标系那这里就是经纬度如果是投影坐标系就得填对应的投影坐标。GeosetNameDlg.cpp这个对话框从名字看是让用户输入 Geoset 名称或路径的。常见做法是弹出一个文件对话框让用户选.gst文件然后把路径传给地图对象。这个对话框的DoModal返回值判断和UpdateData调用是容易出问题的地方后面避坑章节会细说。2.3 编译环境的硬性条件VC6 工程在现代 Windows 上编译第一道坎是 IDE 本身。VC6 在 Win10/Win11 上安装后编译 MFC 工程经常遇到afxwin.h找不到或者链接mfc42.lib失败。常见做法是装一个 VC6 的 SP6 补丁然后把平台 SDK 的包含路径和库路径配好。第二道坎是 MapX 组件。MAPX.H和MAPX.CPP只是封装层真正的 MapX 运行时需要单独安装并注册。如果系统里没有注册 MapX 的 ActiveX 控件Create调用会直接失败。检查方法是看注册表里有没有对应的 CLSID或者用一个简单的 HTML 页面嵌入 MapX 控件测试。第三道坎是字符集。VC6 默认用 MBCS工程里的_T()宏在 MBCS 下就是普通char*。如果 MapX 的接口期望BSTR或LPCTSTR传参时要注意转换。MAPX.CPP里如果有CString到BSTR的转换用的是AllocSysString用完必须SysFreeString否则内存泄漏。# 检查 MapX 控件是否已注册在 cmd 中执行 reg query HKCR\CLSID /s /f MapX | findstr /i MapX # 如果没有输出说明未注册需要先安装 MapX 运行时 # 安装后可以用 regsvr32 手动注册路径以实际安装位置为准 regsvr32 C:\Program Files\Common Files\MapInfo\MapX\MapX50.DLL这段命令的逻辑是先在注册表里搜 MapX 相关的 CLSID确认组件是否已注册。如果没有就需要找到 MapX 的 DLL 并手动注册。注意regsvr32注册的 DLL 必须是 32 位的因为 VC6 编译出来的是 32 位程序64 位 DLL 注册了也没用。3. 地图缩放与选择功能的实现细节坐标转换是绕不过去的坎3.1 缩放功能的瓦片分层逻辑电子地图的缩放底层逻辑是瓦片分层。不同缩放级别对应不同分辨率的瓦片图片放大时加载更细的瓦片缩小时加载更粗的瓦片。GeoExpert 通过 MapX 封装了这套逻辑开发者调用SetZoom时MapX 内部会根据当前缩放值决定加载哪一层瓦片。但这里有个容易误解的地方SetZoom的参数不是“第几级”而是一个地图单位值。MapX 的缩放范围由 Geoset 的坐标系决定。如果 Geoset 是经纬度坐标系缩放值 1 表示整个地球缩放值 0.001 表示街区级别。如果 Geoset 是投影坐标系比如以米为单位缩放值 1000 表示视野宽度约 1000 米。我一般会先读 Geoset 的坐标系定义再决定缩放值的范围。在GeoExpertView.cpp里缩放通常绑定到工具栏按钮或鼠标滚轮。工具栏按钮的响应函数里会先获取当前缩放值乘以一个系数再设回去。// 放大操作当前缩放值乘以 0.5视野缩小一半 void CGeoExpertView::OnMapZoomIn() { double dCurrentZoom m_ctrlMap.GetZoom(); double dNewZoom dCurrentZoom * 0.5; // 限制最小缩放值防止缩放到无意义级别 if (dNewZoom 0.0001) dNewZoom 0.0001; m_ctrlMap.SetZoom(dNewZoom); } // 缩小操作当前缩放值乘以 2视野扩大一倍 void CGeoExpertView::OnMapZoomOut() { double dCurrentZoom m_ctrlMap.GetZoom(); double dNewZoom dCurrentZoom * 2.0; // 限制最大缩放值 if (dNewZoom 1000000) dNewZoom 1000000; m_ctrlMap.SetZoom(dNewZoom); }这里的系数 0.5 和 2.0 是经验值保证每次缩放视觉上变化明显但不突兀。最小和最大缩放值的限制很重要不限制的话用户连续点放大按钮缩放值趋近于零地图渲染会出问题轻则白屏重则崩溃。注意GetZoom和SetZoom操作的是地图对象如果地图对象还没创建或者 Geoset 还没加载这两个调用会返回无效值或直接抛异常。在调用前加一个m_ctrlMap.GetMapState()判断是稳妥做法。3.2 地图选择的坐标转换与几何对象操作地图选择功能用户用鼠标点一下或者拉一个框程序要判断选中了哪些地物。这里涉及两个坐标系屏幕坐标系和地理坐标系。鼠标事件拿到的是屏幕坐标像素地图上的地物用的是地理坐标经纬度或投影坐标。中间需要一个转换函数。MapX 提供了ScreenToMap之类的接口做转换。在GeoExpertView.cpp的鼠标响应函数里先拿到鼠标点转成地图坐标然后构造一个选择工具点选或框选调用地图的Selection相关方法。// 鼠标左键点击选择地物 void CGeoExpertView::OnLButtonDown(UINT nFlags, CPoint point) { // 将屏幕坐标转换为地图坐标 double dMapX 0.0, dMapY 0.0; m_ctrlMap.ScreenToMap(point.x, point.y, dMapX, dMapY); // 构造一个点选择工具容差 5 个像素 m_ctrlMap.SetSelectionTool(selPoint, 5); // 执行选择选择结果放入 Selection 对象 m_ctrlMap.SelectByPoint(dMapX, dMapY); // 获取选中地物的数量 long lCount m_ctrlMap.GetSelectionCount(); if (lCount 0) { // 遍历选中地物读取属性 for (long i 1; i lCount; i) { CString strName m_ctrlMap.GetSelectionName(i); // 将 strName 显示到属性面板或状态栏 } } CView::OnLButtonDown(nFlags, point); }ScreenToMap的转换精度受当前缩放级别影响。缩放级别很大时视野很窄一个像素对应的地理范围很小点选精度高缩放级别很小时视野很宽一个像素对应很大地理范围点选容易误选。容差参数 5 表示在屏幕 5 个像素范围内搜索地物这个值可以根据实际体验调整。框选比点选复杂一些。框选需要记录鼠标按下和抬起的两个点构造一个矩形然后调用SelectByRect之类的接口。矩形框在屏幕上画出来但传给地图对象时要用地图坐标。所以按下时记录屏幕起点抬起时把起点和终点都转成地图坐标再构造矩形。// 框选鼠标按下记录起点抬起时构造矩形选择 void CGeoExpertView::OnLButtonUp(UINT nFlags, CPoint point) { if (m_bIsSelecting) // m_bIsSelecting 在 OnLButtonDown 中置为 true { double dStartX 0.0, dStartY 0.0; double dEndX 0.0, dEndY 0.0; // 起点和终点都转成地图坐标 m_ctrlMap.ScreenToMap(m_ptSelectStart.x, m_ptSelectStart.y, dStartX, dStartY); m_ctrlMap.ScreenToMap(point.x, point.y, dEndX, dEndY); // 构造矩形并执行选择 m_ctrlMap.SelectByRect(dStartX, dStartY, dEndX, dEndY); m_bIsSelecting false; } CView::OnLButtonUp(nFlags, point); }框选时如果起点和终点太近矩形面积趋近于零选择结果可能为空。常见做法是加一个最小像素距离判断小于 3 个像素就当点选处理。另外框选过程中要在屏幕上画一个虚线矩形给用户反馈这需要在OnMouseMove里处理重绘用CDC的DrawFocusRect或者Rectangle画。3.3 Geoset 切换与图层控制GeosetNameDlg.cpp对应的功能是让用户切换 Geoset。Geoset 是 MapX 里地图数据的组织单位一个.gst文件定义了一组图层和它们的显示样式。切换 Geoset 相当于换了一整套地图数据。切换的流程是弹出对话框让用户选.gst文件拿到路径后调用SetGeoSet然后重置缩放和中心点。这里有个坑SetGeoSet之后之前的选择集、图层可见性设置都会失效需要重新初始化。// GeosetNameDlg.cpp 中获取用户选择的 Geoset 路径 void CGeosetNameDlg::OnOK() { UpdateData(TRUE); // 将控件内容同步到成员变量 if (m_strGeosetPath.IsEmpty()) { AfxMessageBox(_T(请先选择 Geoset 文件)); return; } // 检查文件是否存在 CFileFind finder; if (!finder.FindFile(m_strGeosetPath)) { AfxMessageBox(_T(指定的 Geoset 文件不存在)); return; } CDialog::OnOK(); }UpdateData(TRUE)把对话框控件里的值刷到成员变量这是 MFC 对话框的标准操作。但要注意如果对话框里用了CFileDialog来选文件CFileDialog返回后要手动把路径赋给成员变量再调UpdateData(FALSE)刷新控件显示。顺序搞反了成员变量里是空字符串后面判断就过不了。图层控制是另一个常用功能。MapX 的图层对象可以设置Visible属性来控制显示隐藏。在GeoExpertView.cpp里通常会有一个图层列表控件用户勾选或取消勾选时遍历图层设置可见性。// 根据图层名设置可见性 void CGeoExpertView::SetLayerVisible(LPCTSTR lpszLayerName, BOOL bVisible) { // 遍历所有图层找到名称匹配的 long lLayerCount m_ctrlMap.GetLayerCount(); for (long i 1; i lLayerCount; i) { CString strName m_ctrlMap.GetLayerName(i); if (strName.CompareNoCase(lpszLayerName) 0) { m_ctrlMap.SetLayerVisible(i, bVisible); break; } } }图层名匹配用CompareNoCase忽略大小写因为 Geoset 里定义的图层名和代码里写的字符串大小写可能不一致。遍历时图层索引从 1 开始不是 0这是 MapX 的约定写成 0 会取不到图层。4. 避坑与排查VC6 MapX 组合下最容易翻车的五个点4.1 编译报错“无法打开 afxwin.h”现象在 VC6 里打开.dsw按 F7 编译输出窗口报fatal error C1083: Cannot open include file: afxwin.h: No such file or directory。原因VC6 安装时没有勾选 MFC 组件或者安装后环境变量被其他版本的 VS 覆盖了。VC6 的 MFC 头文件在VC98\MFC\Include目录下如果这个目录不在 IDE 的包含路径里就找不到。解决打开 VC6 的Tools - Options - Directories在Include files里加上C:\Program Files\Microsoft Visual Studio\VC98\MFC\Include和C:\Program Files\Microsoft Visual Studio\VC98\ATL\Include。如果系统里同时装了 VS2008 或更高版本确保 VC6 的路径排在前面否则会用到新版本的 MFC 头文件版本不兼容。4.2 链接报错“unresolved external symbol”现象编译通过链接时报一堆unresolved external symbol符号名里带MapX或CMapX前缀。原因MAPX.CPP编译出来的.obj没有参与链接或者 MapX 的导入库没有加到工程里。VC6 工程的链接设置里Object/library modules一栏需要列出所有依赖的.lib文件。解决检查Project - Settings - Link选项卡确认MAPX.obj在编译输出里并且Object/library modules里加了 MapX 的导入库通常是MapX50.lib或类似名称。如果导入库找不到可以从 MapX 安装目录的Lib子目录里复制一份到工程目录然后用相对路径引用。4.3 运行时地图控件创建失败现象程序能启动但主窗口里地图区域是空白的或者弹出一个消息框说“地图控件创建失败”。原因MapX 的 ActiveX 控件没有注册或者注册的是 64 位版本而程序是 32 位。VC6 编译出来的是 32 位程序只能加载 32 位的 ActiveX 控件。解决先用reg query确认 MapX 的 CLSID 是否在注册表里。如果没有找到 MapX 安装目录下的 32 位 DLL用regsvr32注册。如果注册了还是失败检查 DLL 的位数在 64 位系统上regsvr32默认注册 64 位 DLL要注册 32 位 DLL 需要用C:\Windows\SysWOW64\regsvr32.exe。# 用 32 位 regsvr32 注册 MapX 控件 C:\Windows\SysWOW64\regsvr32.exe C:\Program Files (x86)\Common Files\MapInfo\MapX\MapX50.DLL4.4 Geoset 加载失败但没有任何提示现象调用SetGeoSet后地图区域仍然空白程序不报错但也没有地图显示。原因SetGeoSet的路径不对或者.gst文件里引用的图层数据文件比如.tab、.dat、.map不在预期位置。MapX 加载 Geoset 时如果找不到图层数据文件可能静默失败。解决先用绝对路径测试确认.gst文件本身能加载。然后检查.gst文件里引用的图层路径是绝对路径还是相对路径。如果是相对路径工作目录必须和.gst文件所在目录一致。可以在程序启动时用SetCurrentDirectory把工作目录设到 Geoset 所在目录。// 在应用初始化时设置工作目录 TCHAR szPath[MAX_PATH] {0}; GetModuleFileName(NULL, szPath, MAX_PATH); // 去掉文件名保留目录 CString strDir szPath; int nPos strDir.ReverseFind(_T(\\)); if (nPos ! -1) strDir strDir.Left(nPos); SetCurrentDirectory(strDir);4.5 选择操作返回空结果现象鼠标点击地图上的地物GetSelectionCount返回 0但肉眼能看到地物就在点击位置。原因坐标转换出错或者选择容差太小或者地物所在的图层被设置为不可选择。MapX 的图层有一个Selectable属性如果为FALSE该图层上的地物不会被选中。解决先确认ScreenToMap转换后的坐标是否落在预期范围内。可以在转换后把坐标打印出来和地物的实际坐标对比。然后检查目标图层的Selectable属性确保为TRUE。最后适当增大选择容差从 5 个像素调到 10 个像素试试。// 确保图层可选择 m_ctrlMap.SetLayerSelectable(lLayerIndex, TRUE);5. 进阶技巧用调试输出定位地图渲染问题地图渲染出问题时界面上的表现往往是“白屏”或“花屏”但具体是哪一步出的错光看界面看不出来。我一般会在关键调用前后加调试输出把地图对象的状态打出来。VC6 里用TRACE宏输出到 IDE 的调试窗口。// 在关键调用前后加 TRACE观察状态变化 TRACE(_T(Before SetGeoSet: path%s\n), strGeosetPath); BOOL bRet m_ctrlMap.SetGeoSet(strGeosetPath); TRACE(_T(SetGeoSet returned: %d\n), bRet); double dZoom m_ctrlMap.GetZoom(); TRACE(_T(Current zoom: %f\n), dZoom); long lLayerCount m_ctrlMap.GetLayerCount(); TRACE(_T(Layer count: %d\n), lLayerCount); for (long i 1; i lLayerCount; i) { CString strName m_ctrlMap.GetLayerName(i); BOOL bVisible m_ctrlMap.GetLayerVisible(i); TRACE(_T(Layer %d: name%s, visible%d\n), i, strName, bVisible); }这段调试代码的价值在于SetGeoSet的返回值能告诉你加载是否成功GetZoom能告诉你当前缩放值是否在合理范围图层数量和每个图层的可见性能告诉你 Geoset 里到底有什么、哪些被隐藏了。如果SetGeoSet返回FALSE问题在路径或文件本身如果返回TRUE但图层数为 0问题在.gst文件的内容如果图层数正常但全部不可见问题在图层可见性设置。另一个技巧是用 MapX 的ExportMap接口把当前地图导出成图片直接看渲染结果。这比盯着屏幕猜要快得多。// 导出当前地图为图片用于排查渲染问题 m_ctrlMap.ExportMap(_T(C:\\Temp\\map_debug.bmp), 800, 600);导出图片的尺寸参数可以设大一点比如 1600x1200这样细节更清楚。导出后打开图片如果图片是空白的说明地图数据没加载如果图片有内容但屏幕上是空白说明是窗口重绘或控件尺寸的问题。从那以后我每次拿到这种 VC6 MapX 的老工程都强制走一遍“删中间文件 → 配包含路径 → 注册控件 → 设工作目录 → 加 TRACE 输出”的流程基本能在半小时内判断出这个包能不能跑、缺什么、改哪里。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?