
简介陈建春《用Visual C开发GIS系统》一书的配套代码包面向初学MFC或希望入门VC GIS开发的读者既可作为图解MFC窗口、菜单、对话框、文档视图架构的练习素材也可借此熟悉矢量图形绘制与简单数据管理流程。压缩包共90个文件整体仅144KB以bmp图标与图片、cpp/h源码、rtf帮助文档、txt说明为主工程文件齐全适合对照书籍章节手动编译与断点调试。目前已有302人学习下载。代码虽小巧但覆盖了MFC单文档程序从界面搭建到图形交互的基础流程还包含数据库文件与撤销操作等设计作者也点明其在空间索引、投影、大数据量管理方面的不足读者可将它作为入门模板再结合GDAL/ArcObjects等库扩展从而更系统地理解真实GIS系统的架构演进。 做GIS开发有些年头的人大概都绕不过陈建春这个人的名字。他的《用Visual C开发GIS系统》那本书和配套代码可以说是很多老GIS开发者的启蒙教材。前几天整理硬盘又翻出这套VC的GIS配套代码随手编译试了试居然还能跑起来一下子勾起了不少回忆。今天就把这套代码的来龙去脉、技术结构、移植经验和它真正值得学习的地方好好聊一遍。1. 这本书和这套代码凭什么被大家记到现在1.1 在那个GIS开发还没有标准化框架的年代2000年前后GIS开发是什么状态ArcGIS还没有像今天这样一家独大MapInfo、SuperMap都在各自生长WebGIS更是连影子都没有。那时候想做一个带地图显示、缩放、查询功能的桌面程序最主流的路线就是Visual C 6.0加MFC配合自己解析各种数据格式。陈建春这本书恰好踩在这个时间节点上把一套相对完整、能编译、能运行的GIS系统源码公开了出来。我当时拿到配套光盘的时候第一反应是这代码怎么这么厚。解压之后发现工程文件整整齐齐有栅格数据处理、矢量图形编辑、属性数据库管理、专题图制作甚至还有简单的空间分析功能。对一个没接触过GIS底层算法的人来说这些东西比看十篇论文都管用——因为它是能跑的能断点调试的能自己改着玩的。1.2 为什么二十年过去了还有人翻出这套老代码这就要说实话了。现在网上一搜Visual C和GIS相关的关键词排在前面的往往还是这套配套代码的下载链接或使用问题。原因很简单现在主流GIS开发已经高度封装化了大家用ArcEngine、SuperMap Objects、MapWinGIS之类的组件拖拖控件就能出图但很多人并不真正理解地图是怎么画出来的、数据是怎么组织起来的。如果你要参加GIS开发岗位面试或者要自己写一个轻量级的GIS显示模块这套老代码反而是最好的参考资料。它不依赖任何商业组件纯靠VC的GDI、MFC的文档视图结构、以及自己写的数据读取和算法类把一套微型GIS系统从零造了出来。这种从底层造轮子的思路恰恰是今天很多教程缺失的部分。2. 配套代码的工程骨架MFC架构和GIS数据结构是怎么结合的2.1 典型的MFC单文档程序却被赋予了GIS内核这套代码的工程形态本质上是Visual C 6.0时代最经典的MFC单文档视图架构。但作者在框架内做了大量GIS化扩展这也是我建议初学者重点学习的地方——它告诉你如何在一个通用GUI框架里嵌入领域逻辑。核心的类设计可以大概分成四层显示层从CView派生的视图类负责地图的绘制、缩放、漫游、刷新。数据层从CDocument派生的文档类管理GIS数据的加载、存储、序列化。算法层一系列独立的功能类比如坐标转换、点线面关系判断、缓冲区分析等。交互层通过MFC的消息映射机制把鼠标操作转换为地图编辑和查询命令。这套分层不是死的但它的好处在于每个类职责清晰你要改某个算法不会牵一发而动全身。我在后来的实际项目中包括用Qt重写GIS客户端时骨子里用的还是这套思路。2.2 数据文件解析自己写代码读GIS数据而不是靠第三方库配套代码里最硬核的部分是对多种GIS数据格式的解析。那个年代还没有GDAL/OGR一统天下作者是直接按照公开的文件格式规范用C的二进制文件流读取数据的。比如Shapefile要分别处理.shp几何坐标、.shx索引、.dbf属性三个文件栅格数据则涉及BMP、TIFF等格式的解析和显示。这种做法的学习价值远大于直接调用库函数。你用GDAL打开一个Shapefile就是一行代码但你不清楚文件头里那些字段分别代表什么不知道坐标记录是怎么压缩存储的。而读这套代码你会真正理解一个点要素在文件里存储的是两个double类型的坐标一条线要素的坐标点数是如何记录的面要素的边界坐标串是如何闭合的。理解了这些之后再回去用GDAL你会发现很多问题自己能推导出来比如为什么有的Shapefile读取会丢失字段为什么坐标值出现巨大偏移多半就是文件解析时的字节序或者偏移量出了问题。2.3 文档视图结构在地图应用里的天然优势MFC的Document/View架构放在GIS场景下其实是很顺手的。视图类只管怎么画文档类管数据和业务规则。地图窗口尺寸改变时只需要在视图的OnDraw或OnPaint里根据当前显示范围重新绘制即可数据更新时调用UpdateAllViews通知所有关联视图刷新。配套代码在这一点上做得非常干净。它的视图类里维护了一个当前显示范围的矩形对象所有绘制操作都以这个矩形为基准做裁剪和坐标映射。缩放操作本质上是修改这个显示范围矩形然后强制刷新视图。这个逻辑几乎被后来的所有GIS界面框架继承下来包括我接触过的很多商业组件内部也是这套机制。3. 把陈年老代码跑起来VC6到VS2022的移植实操记录3.1 别急着打开工程先建一个新项目把源文件加进去如果你拿到的是VC6版的.dsw/.dsp工程文件直接用Visual Studio 2022打开会提示版本太旧虽然VS会自动做一次升级但很多时候会留下一堆兼容性问题。我的习惯是新建一个MFC应用程序工程然后把书里的源码目录下的.cpp、.h文件拷贝到新工程里酌情排除掉一些与核心功能无关的旧代码文件。这样做的原因很实际旧工程的编译选项、预编译头设置、字符集定义和新版IDE差异很大直接升级容易在链接阶段出现各种莫名其妙的符号错误。而建新工程虽然前期要花点时间配置但编译过程是可控的出了问题也知道往哪查。3.2 一定会遇到的三个编译错误和解决办法我实地踩过的坑按出现的频率排序大概是这三位第一个是for循环变量作用域问题。VC6默认是C98标准for(int i0;...;i)里的i在循环体外部还能用而VS2019和VS2022采用较新的C标准后i的作用域被限制在循环内部。配套代码里很多地方在循环外继续使用循环变量会直接报未声明的标识符。解决办法是在每个使用点前面重新声明一下或者把循环改成while写法。这个改动不复杂但数量很多要有耐心逐个处理。第二个是字符集问题。旧代码很多地方用char字符串和CStringA新工程默认是Unicode字符集会导致CString和const char*之间的传参报错。我的处理方式是把工程属性里的字符集改成使用多字节字符集这样能最大程度保留原代码的逻辑避免大量TEXT宏和CStringW的改动。如果确实需要Unicode就得把字符串操作的那一层单独抽出来重构。第三个是标准库头文件的顺序。老代码常常依赖某种隐式的头文件包含顺序尤其是assert.h、stdlib.h这类在Clean环境下编译会提示找不到定义。解决方法是把常用的标准头文件统一放到预编译头stdafx.h里并且在每个.cpp文件开头按固定顺序包含。3.3 第三方库依赖的坑配套代码里有一部分功能用到了第三方的库比如空间分析可能需要一些数学库图像处理用了GDI或者不同版本的GDI函数。这些库在新系统上的头文件和.lib路径都要重新配置。我的经验是配置完成后先单独编译每个模块的Release版本能过再整体编译。因为Debug版和Release版的运行库不同如果混用链接时会报类似于libcmtd.lib和libcmt.lib冲突的LNK2005错误。出现这种问题就是运行时库设置不一致统一改成多线程(/MT)或根据你自己项目的需要统一设置即可。4. 代码里藏着的GIS底层原理值得反复咀嚼的算法和设计4.1 栅格数据的显示DIB和调色板配套代码里栅格图像的显示部分我在很多项目里都参考过。它使用设备无关位图DIB作为内存中的图像载体通过读取图像文件的像素数据和调色板信息在窗口中按用户设定的缩放级别进行绘制。核心思路是先创建一块与显示区域大小匹配的内存DC把地图内容全部绘制到内存DC上最后一次性地BitBlt到窗口DC。这种双缓冲技术在今天看起来是基础但在那个年代很多程序直接在OnDraw里逐像素SetPixel导致地图缩放时一卡一卡的。这套代码让我意识到好的地图交互体验和底层绘制优化密不可分。如果你要做高性能的栅格显示还可以进一步把DIB替换为Direct2D或OpenGL纹理但基本的先整幅离屏渲染再整体呈现的思路依然是效率最高的。特别是大影像数据一次只绘制当前视口范围内的瓦片或像素块这个优化思路在这套代码里其实已经能看到雏形。4.2 矢量编辑的交互精髓命中测试和尖锐角处理矢量要素的绘制和编辑是配套代码里最有价值的部分之一。它实现了鼠标点击选点、拖拽移动、双击结束绘图等一套完整的交互流程。这里的核心是命中测试算法判断鼠标点击位置是否落在某个点、某条线或某个面上面。比如要判断点是否被选中通常不是判断屏幕上的点坐标完全重合而是判断鼠标位置和要素坐标的距离是否小于一个像素容差。线段和多边形的命中测试则需要计算点到线段的距离或者判断点是否在多边形内部。这套代码里用到的射线法判断点在多边形内逻辑写得很清晰边界情况处理也比较到位。再说说热搜词里总有人问的GIS尖锐角处理一般角度多大。这个问题在矢量数据编辑里确实很常见。尖锐角指的是相邻两条线段夹角过小的情况这个角在图形上表现为一个很尖的刺。一般做拓扑检查时会把小于某个角度阈值的尖锐角标出来。角度设多大没有统一的国际标准常见的是5度或者10度有时候还要结合制图比例尺来定。配套代码虽然没有把尖锐角检查作为一个单独模块实现但它的角度计算函数已经提供了判断两条线段夹角的底层能力你要做尖锐角检查直接在这个函数的基础上加一个阈值判断即可。4.3 空间分析功能缓冲区、叠加和属性查询配套代码里的空间分析模块用现在的眼光看比较简单但麻雀虽小五脏俱全。它实现了点、线、面要素的基本缓冲区算法以及要素间的叠加裁剪。缓冲区的实现方式是先对每条线段生成左右两条平行线然后对相邻平行线求交点最后对外侧角点做圆弧处理。这个过程虽然计算量不小但逻辑非常直接。这里我想多说一句很多人用ArcGIS用习惯了以为缓冲区分析就是选中图层点一下那个工具。实际上如果你要做的场景比较复杂比如要做动态缓冲区、要按属性字段设置不同缓冲半径、要处理多个图层之间的并集和差集理解底层算法能让你更快地定位问题。我遇到过缓冲区结果边缘出现锯齿的情况就是因为在求平行线交点时没有正确处理线段的方向向量导致部分外侧角点顺序错乱。这种问题没有算法基础的人根本无从下手。属性数据库管理用的是Access或dBase格式配套代码封装了一套字段定义、记录增删改查、与图形要素ID关联的机制。这套东西在核心设计上已经很接近现代GIS中属性表空间索引的雏形了。它让我明白了GIS和普通CAD软件的本质差别GIS的图形要素必须与属性数据绑定并且能够互相查询。你选中一个面能查出它的面积和权属你选中一条记录地图上能高亮对应的图形——这个双向联动机制从这套代码开始就一直贯穿在我做的所有GIS项目里。5. 这套老代码在今天的延伸用法从MFC走向跨平台和Web5.1 从MFC到Qt和WebGIS的架构映射你可能会问MFC都过时了学这套代码还有意义吗我的看法是框架会过时但架构思想不会。如果你掌握了这套代码里的分层设计和核心数据结构组织方式把它迁移到Qt上其实非常快。MFC的CView对应Qt的QGraphicsView或者QWidgetCDocument对应Model类消息映射对应信号槽。我有一次做一个Electron版的地图标注工具浏览器前端需要显示矢量图层、执行空间查询我在设计通信协议时就参考了这套老代码里的图层-要素-属性三层模型。每一次从服务端拉取数据前端构建要素集合再绑定属性数据和当年在MFC里做的事情本质上没有区别。5.2 当算法字典用而不是当工程模板抄最实在的用法是把这套代码当一本活字典在需要解决具体问题时再去翻对应章节。比如你突然需要写一个坐标点旋转的算法或者需要一个把经纬度坐标转成屏幕坐标的方法翻配套代码的相关函数往往能得到一个简洁的参考实现。因为这些基础几何算法二十年来几乎没有变化。但如果想把它整体改造成一个现代GIS系统我不建议在原代码上硬改。更好的路线是保留它的核心算法类和数据结构设计思想用现代C重新实现一遍比如用CMake管理工程、用标准库容器替换MFC的CArray和CString、把UI层换成Qt或DuiLib再接入现代的地图投影库和空间索引库。这样既能利用老代码的算法积淀又能获得现代开发效率。5.3 一点小小提醒最后提醒一下初学者。这套配套代码的质量在当年属于上乘但它毕竟受限于那个时代的编译器和开发环境一些内存管理方式在今天看已经不太规范。比如new出来的对象在某些异常分支上可能没有delete字符串拼接也没有做充分的越界检查。我建议你在阅读时重点看算法实现和整体架构不要照着它的代码风格去写新代码尤其要注意自己做内存管理和边界检查。我在编译过程中其实还遇到过一个很隐蔽的问题。旧代码里有个别函数使用了硬件相关的内联汇编这部分在新版64位编译器下根本无法编译。面对这种情况我的建议是直接看这个函数的功能再用普通C重新实现一遍。本质上就是用数学库函数替代汇编指令效果完全一样但可移植性和可读性都更好。这本书和这套代码陪伴了国内好多GIS开发者的成长。哪怕放到今天它依然是一份很珍贵的学习资料。它教你的是怎么把一个纯粹的C程序员变成能理解地图、理解空间数据的人。我希望你看完这篇文章后也能从硬盘角落翻出这套代码动手编译一次体验一下那个没有GIS引擎、全靠自己写代码画出第一张地图的年代。本文还有配套的精品资源点击获取