Visual C++ GIS系统开发:经典MFC代码架构与底层算法解析
2026/9/3 1:44:55 网站建设 项目流程

简介:陈建春《用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默认是C++98标准,for(int i=0;...;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走向跨平台和Web

5.1 从MFC到Qt和WebGIS的架构映射

你可能会问,MFC都过时了,学这套代码还有意义吗?我的看法是,框架会过时,但架构思想不会。如果你掌握了这套代码里的分层设计和核心数据结构组织方式,把它迁移到Qt上其实非常快。MFC的CView对应Qt的QGraphicsView或者QWidget;CDocument对应Model类;消息映射对应信号槽。

我有一次做一个Electron版的地图标注工具,浏览器前端需要显示矢量图层、执行空间查询,我在设计通信协议时,就参考了这套老代码里的"图层-要素-属性"三层模型。每一次从服务端拉取数据,前端构建要素集合,再绑定属性数据,和当年在MFC里做的事情本质上没有区别。

5.2 当算法字典用,而不是当工程模板抄

最实在的用法,是把这套代码当一本"活字典",在需要解决具体问题时再去翻对应章节。比如你突然需要写一个坐标点旋转的算法,或者需要一个把经纬度坐标转成屏幕坐标的方法,翻配套代码的相关函数,往往能得到一个简洁的参考实现。因为这些基础几何算法,二十年来几乎没有变化。

但如果想把它整体改造成一个现代GIS系统,我不建议在原代码上硬改。更好的路线是:保留它的核心算法类和数据结构设计思想,用现代C++重新实现一遍,比如用CMake管理工程、用标准库容器替换MFC的CArray和CString、把UI层换成Qt或DuiLib,再接入现代的地图投影库和空间索引库。这样既能利用老代码的算法积淀,又能获得现代开发效率。

5.3 一点小小提醒

最后提醒一下初学者。这套配套代码的质量在当年属于上乘,但它毕竟受限于那个时代的编译器和开发环境,一些内存管理方式在今天看已经不太规范。比如new出来的对象在某些异常分支上可能没有delete,字符串拼接也没有做充分的越界检查。我建议你在阅读时,重点看算法实现和整体架构,不要照着它的代码风格去写新代码,尤其要注意自己做内存管理和边界检查。

我在编译过程中,其实还遇到过一个很隐蔽的问题。旧代码里有个别函数使用了硬件相关的内联汇编,这部分在新版64位编译器下根本无法编译。面对这种情况,我的建议是直接看这个函数的功能,再用普通C++重新实现一遍。本质上就是用数学库函数替代汇编指令,效果完全一样,但可移植性和可读性都更好。

这本书和这套代码,陪伴了国内好多GIS开发者的成长。哪怕放到今天,它依然是一份很珍贵的学习资料。它教你的是怎么把一个纯粹的C++程序员,变成能理解地图、理解空间数据的人。我希望你看完这篇文章后,也能从硬盘角落翻出这套代码,动手编译一次,体验一下那个没有GIS引擎、全靠自己写代码画出第一张地图的年代。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询