简介:ShpEditor_x 是一套基于 Visual C++ 开发的 Shapefile 矢量数据编辑工具,面向 GIS 开发人员与地理信息专业学习者,解决了在 Windows 环境下对点、线、多边形等矢量要素进行创建、修改、删除及属性编辑的实际需求,适用于国土规划、测绘数据处理及 GIS 教学演示等场景。压缩包共含 167 个文件,约 1.43MB,核心代码以 53 个 h 头文件和 49 个 cpp 源文件构成,便于阅读工程结构与二次开发;同时附带 24 个图标、22 个位图资源及 dsp、dsw 等 VC++ 工程配置文件,整体是一套完整可编译的编辑工具源码。已有 335 人浏览学习,适合需要掌握 Shapefile 底层读写逻辑、矢量编辑界面搭建或 VC++ GIS 编程方法的读者。通过源码可了解要素拾取、几何变换、属性表关联等核心功能的实现思路,也可直接编译运行进行功能扩展。
1. 还在用 VC 写 SHP 编辑器:这块老骨头到底能解决什么
你手头压着三调、国土变更这类 SHP 整理的活儿,电脑里又没有完整 GIS,想改一个图斑属性得等半分钟加载。甲方丢来一个Shp__Editor__x.rar,解压开是个绿色小工具——内行一眼能看出,这是用 Visual C++ 编译的 SHP 矢量编辑工具。它不负责出图,也不做投影变换,任务很窄:直接读写 ESRI Shapefile 的几何与属性,让你在没有重型软件的情况下改节点、换属性、批量替换字段。适合测绘内业、GIS 二次开发和维护老系统的人。这篇文章从 SHP 二进制结构讲到 VC 编译接入,再落到数据转换与避坑,目标是让你照着复现一遍不会翻车。
2. 拆开 SHP 的二进制结构:为什么必须动这三件套
2.1 一个 SHP 其实是三个文件:shp、shx、dbf
ESRI Shapefile 名字里只有一个 SHP,磁盘上却是同一主名下的多个物理文件。shp放几何坐标,shx是几何记录的位置索引,dbf放属性。很多编辑事故都出在“只改了 shp 或只改了 dbf”——交回 ArcGIS 时提示文件损坏,要素错位,属性对不上。
shx本身是一张查找表:记录号、记录起始字节、内容长度。GIS 打开文件时先读这张表,再按表里的偏移去取几何。如果你在编辑器中增删了记录,却没有同步重写shx,表里的偏移值和shp里的实际位置对不上,就会出现要素顺序错乱、丢要素这类抽象问题。经验之谈是:任何一次结构级修改,都把这四个文件一起处理。除了上面三个,还要注意prj(坐标系 WKT 文本)和cpg(代码页说明)。只编辑几何和属性时,很多老工具不会动prj,它保留原样反而是安全行为;但如果你用编辑器新建文件,它通常只生成纯几何数据,prj需要事后自己补。
2.2 文件头 100 字节里,最容易被改坏的是范围字段
第一次用十六进制编辑器看 SHP 的 100 字节文件头,会有一种“规格说明书摆在眼前”的感觉。文件开头固定是文件代码 9994,第 24 到 27 字节是文件总长度,第 32 到 35 字节是形状类型,第 36 到 67 字节是整个文件的最小外接矩形,也就是 Xmin、Ymin、Xmax、Ymax 四个 double。
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 4 | 文件代码 | 固定大端 9994 |
| 24 | 4 | 文件长度 | 以 16 位字为单位 |
| 28 | 4 | 版本 | 固定 1000 |
| 32 | 4 | 形状类型 | 0=空、1=点、3=折线、5=多边形 |
| 36 | 32 | 全图范围 | Xmin、Ymin、Xmax、Ymax |
| 100 | — | 记录区 | 真正的几何数据从第 100 字节开始 |
真正的几何数据从第 100 字节开始。每条记录前有一个 8 字节的记录头:前 4 字节是内容相对文件头的偏移,后 4 字节是内容长度。这里有个历史遗留坑:长度按 16 位字为单位,不是按字节。自己去读二进制时,算长度要乘 2,不少人在这里栽过跟头。
再往下一层是形状体。点坐标只有 X/Y;折线和多边形则包含 bbox、点数组、parts 数组。多边形和折线的二进制结构几乎一样,差别只在于多边形要求首尾点闭合,以及 parts 里记录的环方向。很多小编辑器写多边形时忘了闭合首尾,拓扑检查时直接报错。
这正好呼应检索词里的header editor——在 SHP 语境里,它的核心含义就是编辑文件头。最常见的场景是:文件被裁剪、合并或增删记录后,范围字段没有重算。GIS 加载时按旧范围建空间索引,新加进去的要素被当成“越界”裁掉,表现就是“整个图层只剩一个点”。这是矢量编辑里最隐蔽的翻车方式。
2.3 用 VC 开发时为什么绕不开 shapelib
与其自己造轮子,不如直接接上那套成熟的 C 库。几乎所有的老 VC 工具都把 shapelib 打包在工程里,解压Shp__Editor__x.rar后,你会在源码目录里看到shpopen.c、dbfopen.c和shapefil.h这三个文件。它的价值是替你处理了三件事:大端字节序转换、文件头读写、DBF 字段解析。
#include "shapefil.h" void ShowShpInfo(const char *pszBase) { SHPHandle hSHP = SHPOpen(pszBase, "rb"); if (hSHP == NULL) { fprintf(stderr, "SHPOpen 失败: %s\n", pszBase); return; } int nEntities, nShapeType; double adfMinBound[4], adfMaxBound[4]; SHPGetInfo(hSHP, &nEntities, &nShapeType, adfMinBound, adfMaxBound); printf("要素数: %d\n", nEntities); printf("类型: %d (1=点, 3=折线, 5=多边形)\n", nShapeType); printf("范围: %.6f, %.6f -> %.6f, %.6f\n", adfMinBound[0], adfMinBound[1], adfMaxBound[0], adfMaxBound[1]); SHPClose(hSHP); }SHPOpen的第一个参数不带扩展名,shapelib 会自动找.shp、.shx、.dbf。第二个参数用"rb"表示只读,如果想写就用"rb+"。SHPGetInfo的输出参数分别是要素数、形状类型和两个四元素数组。数组开 4 是为了兼容 Z/M 值,前两位永远是 X/Y,二维数据后两位是 0.0。典型循环读取是SHPReadObject逐条拿SHPObject指针,用完SHPDestroyObject释放。
字节序这点值得单独强调:SHP 文件按大端存储,x86 上的 VC 程序是小端。如果自己用fread加memcpy直接转成整型,数字会完全反掉。shapelib 的设计目标之一就是把 XDR 和主机字节序的差异消化掉,让上层只跟 double 数组打交道。
3. 用 VC 编译 SHP 编辑器:从 rar 包到可执行文件
3.1 解压老工具包后,先分清源码和预编译 exe
解压得到一个Shp__Editor__x.rar后,里面通常有两种形态。一种已经编好 exe 和若干 dll,拿来即用;另一种带.dsw或.vcproj源码工程。如果是 VC6 工程,用新版 VS 打开时会弹升级向导,常见问题是 MFC 库缺失或链接器设置不兼容。单纯的控制台工程会好办很多,全部依赖就是 shapelib 的那几个 C 文件。
拿到工程第一步,确认shpopen.c、dbfopen.c、shapefil.h的位置。有些打包人把 shapelib 塞进子目录,编译时需要在 Include 路径里追加。我习惯把 shapelib 源码直接拖进工程,或者把子目录加入附加包含目录。关键是不能让两个shpopen.obj同时参与链接,否则会报重复符号。
还有一个容易被忽略的点:老 rar 包里的 exe 不一定和源码对应。没有作者签名,没有哈希校验,就不要直接拿解压出来的 exe 处理生产数据。实用做法是在隔离环境里先跑一遍,或者干脆用源码重新编译,用自己编出来的版本。
3.2 命令行编译的最小步骤
不走 IDE 时,用 Visual Studio 自带的开发人员命令行最直接。下面这段把 shapelib 编译成静态库,再链接编辑器主程序。
# 打开 Visual Studio Developer Command Prompt,或先执行 vcvars32.bat cd /d D:\Shp_Editor_x # 编译 shapelib 的两个核心 C 文件 cl /c /O2 shpopen.c dbfopen.c # 打包成静态库 lib /out:shapelib.lib shpopen.obj dbfopen.obj # 编译编辑器主程序,/I. 表示当前目录找头文件 cl /O2 /I. editor_main.cpp shapelib.lib /Fe:ShpEditor.exe第一条cl命令里的/O2是速度优化。老代码如果逻辑写得比较“野”,依赖未定义行为,建议降到/O1,省得优化器改变执行顺序后行为不一致。链接阶段如果报unresolved external symbol,先看是不是漏了user32.lib、gdi32.lib。如果编辑器主程序用了 MFC 对话框,还要把 MFC 库路径加进链接器,并把运行时切换成/MD。
编译成功后,拿手头一个小 SHP 文件做冒烟测试:打开、移动一个顶点、保存、再用 QGIS 打开,看几何有没有被破坏。这一步能过滤掉大多数工程配置问题。
3.3 编辑器界面上的参数:类型、字段、坐标系先设对
进入编辑器新建文件时,最先遇到的是几何类型选择。点层选 Point,线层选 PolyLine,面层选 Polygon,这个选错后面没法补救。属性字段的高发坑在字段类型上:要把“乡镇名”做成字段,就得选字符型并给足宽度。DBF 的字符字段是定长存储,比如 NAME 字段宽度 20,写入“观音桥镇”只占前面几个字节,后面自动补空格。如果字段宽度定义成 4,而数据里有“观音桥街道办事处”,写入会被静默截断,你甚至看不到报错。
新建 SHP 时坐标系参数也容易踩坑。老编辑器只认识 ESRI 传统 WKT,比如GCS_WGS_1984。遇到 CGCS2000 或地方 2000 坐标系,老工具可能完全不理解。只要你不主动改 PRJ 文件,原文件的坐标系文本不会被覆盖。所以编辑前把.prj单独备份一份,就是最稳的“后悔药”。需要新建文件时,别手打 WKT,直接从一个同坐标系文件复制.prj过来,比手工拼写可靠得多。
4. 编辑矢量数据的常规路径:节点、属性、批量替换一次打通
4.1 改节点前先搞清点数组和 parts 的关系
手动改坐标是最直观的操作。SHP 多边形/折线内部是一个 points 数组加一个 parts 数组。parts 记录每个环从第几个点开始,一个面可以包含多个环,外环加内环(岛)就靠 parts 切分。典型错误是:删掉几个顶点后,只改了 points 数组,没有同步调整 parts 的起始下标。文件在编辑器里看着正常,拿到 ArcGIS 直接报几何损坏。
正确做法是每次节点增删后,校验nVertices等于所有环内点数之和,parts 数组的最后一个起始点加环内点数不超过nVertices。下面是移动一个要素全部顶点坐标的示例:
#include "shapefil.h" /* 将第 iTarget 个要素的所有顶点 X 坐标加偏移量 */ void ShiftFeatureX(SHPHandle hSHP, int iTarget, double dOffset) { SHPObject *psShape = SHPReadObject(hSHP, iTarget); if (psShape == NULL) return; for (int i = 0; i < psShape->nVertices; i++) { psShape->padfX[i] += dOffset; } psShape->dfXMin += dOffset; psShape->dfXMax += dOffset; /* 创建临时对象并按原位置写回 */ SHPObject *psNew = SHPCreateObject( psShape->nSHPType, psShape->nShapeId, psShape->nParts, psShape->panPartStart, NULL, /* panPartType 传 NULL 表示默认环类型 */ psShape->nVertices, psShape->padfX, psShape->padfY, NULL, NULL); SHPWriteObject(hSHP, iTarget, psNew); SHPDestroyObject(psNew); SHPDestroyObject(psShape); }SHPCreateObject第六个参数传NULL表示让 shapelib 自动推断环类型,这对多边形很重要。写回后,单个要素自己的dfXMin/dfXMax已经更新,但整个文件的头部范围还需要单独重算。这是老工具最容易忘的一步:局部改了坐标,全图范围没跟上,后面的问题跟第 2 章说的header editor是同一类。
4.2 拆面、渔网分割和手工编辑的边界
编辑需求里有一类高频场景是拆面:把一个大图斑按道路或权属线切成若干小图斑。好的编辑器会提供手工画分割线,每次分割生成独立的 polygon。拆完必须检查分割线两端是否精确落在边界上,否则会留下 0.000001 度的细微裂缝。肉眼看不见,叠加分析时漏一块,面积统计对不上。
搜索词里有一个“渔网分割shp”,这个操作通常不用这种小工具,而是 ArcGIS Fishnet 或 QGIS 网格工具。小编辑器真正合适的位置,是在批量切割前做预处理:去掉重复点、剔除背洞、统一边界方向。如果你拿到“县域行政区划边界 SHP 按网格切片”这类的任务,正确路径是先在小工具里把源数据清理干净,再交给能批量切网的软件。工具选错了,后面怎么调都别扭。
4.3 DBF 属性批量替换的完整代码
属性编辑是使用频率更高的工作。替换某个字段里的固定文本,是老式 SHP 编辑器的看家本领。下面这段代码演示如何把指定字段里等于某个旧值的记录替换成新值:
#include "shapefil.h" #include <string.h> int ReplaceFieldValue(DBFHandle hDBF, const char *pszField, const char *pszOld, const char *pszNew) { int nField = DBFGetFieldIndex(hDBF, pszField); if (nField < 0) { fprintf(stderr, "字段不存在: %s\n", pszField); return -1; } int nCount = DBFGetRecordCount(hDBF); int nHits = 0; char szBuf[1024]; for (int i = 0; i < nCount; i++) { DBFReadStringAttribute(hDBF, i, nField, szBuf); if (strcmp(szBuf, pszOld) != 0) continue; /* 先用空格填充,避免残留旧字符串的尾部 */ memset(szBuf, ' ', sizeof(szBuf)); szBuf[sizeof(szBuf) - 1] = '\0'; strncpy(szBuf, pszNew, strlen(pszNew)); DBFWriteStringAttribute(hDBF, i, nField, szBuf); nHits++; } return nHits; }这段代码有两个容易忽略的细节。一是DBFGetFieldIndex返回的字段编号从 0 开始,找不到时返回 -1。二是写回前用空格填充缓冲区,这是处理“旧字符串比新字符串长”的关键:DBF 字段定长存储,如果只写一个短字符串而不填充剩余位,后面读出来的字段就会带着旧尾巴。很多新手问“属性值怎么删不干净”,原因就在这里。
另外,如果字段类型是数值型(N 或 F),不要用DBFWriteStringAttribute,应该用DBFWriteIntegerAttribute或DBFWriteDoubleAttribute。往数值字段里写非数字内容,轻则显示异常,重则统计汇总时报错。
4.4 改完后的三项检查:索引、范围、备份习惯
一个完整修改流程的收尾不是“点保存”,而是验证。我的习惯是关闭编辑器后,把.shp、.shx、.dbf三个文件各复制一份,然后用 QGIS 或独立写的检查工具重新打开,重点看三件事:要素顺序和保存前是否一致;全图坐标范围有没有突变;属性表记录数和几何要素数是否对齐。
如果任何一项对不上,用备份覆盖回去重做,不要试图手工修 SHX 偏移表。老工具往往没有撤销功能,备份就是唯一后悔药。每次执行批量替换之前,另存一个backup_before_修改副本,几分钟的成本换一晚上安稳,这笔账值得算。
5. 矢量编辑避坑:五个保存后翻车现场的排查记录
5.1 现象:打开后只剩一个点,其余要素全消失
保存后拿 ArcGIS 或 QGIS 打开,整个图层只剩一个孤立点。打开属性表一看,记录数还在,就是画不出来。
原因:文件头范围字段没更新。第 2 章提过,文件头第 36 到 67 字节保存全图 Xmin、Ymin、Xmax、Ymax。如果编辑器追加记录时只写几何体,没更新头部范围,GIS 按旧范围建空间索引,新增要素被当成越界数据处理掉。另一种情况是把单个要素的dfXMax写成了 0,导致对象包围盒退化成一个点。
解决:重算整个文件的边界并写回文件头。如果你的 shapelib 版本导出了SHPWriteHeader,直接调用最省事;否则用fseek定位到第 36 字节,把四个 double 按大端字节序覆盖写入。注意 x86 上需要手写字节交换,别直接fwrite一个 double 数组。
5.2 现象:保存后几何和属性错位,要素顺序换乱
编辑完保存,再用软件打开,发现第 3 号要素显示的是第 7 号要素的图形,属性表却还是原顺序。
原因:.shx索引没有同步。SHP 主文件的记录头里,前 4 字节是这条记录相对文件头的偏移。增删记录后,后续所有记录的偏移都变了,但如果只写了.shp没重写.shx,索引表还是老偏移。
解决:用 shapelib 按“读写模式”重新打开一次文件,触发索引重建。最省事的办法是删掉旧的.shx,用 QGIS 重新打开并另存一份,它会自动生成新的索引。如果编辑器自带“重建索引”按钮,优先用它,因为编辑范围只在当前软件里,生成结果更可控。
5.3 现象:中文属性全部乱码,或者字段截断变短
修改完属性,其他人打开一看,中文全成了问号或乱码。另一个常见现象是字段里的文字变短了,后半截消失。
原因:乱码是代码页不匹配。历史数据的 DBF 大多用 GBK 编码,而新版软件可能按 UTF-8 读取,或者反过来。字段截断是字段宽度不够,DBF 字段是定长的,写入超出宽度时被静默截断。
解决:编辑前先确认.cpg文件里写的代码页标识。没有.cpg时,老数据大概率是 GBK,用支持代码页切换的工具重新打开。遇到字段截断,回到定义字段那一步,把字符型字段宽度加大到最大可能值,再重新写入。不要试图在一个窄字段上硬塞长文本,后面统计和符号化都会出问题。
5.4 现象:面与面之间出现细缝,叠加分析漏掉小块
两个相邻多边形原本完美契合,编辑其中一个的边界后,中间出现一条极细的缝隙。叠加重叠统计时,缝隙变成空洞,面积对不上。
原因:相邻边界两边的顶点没有精确捕捉到同一坐标。手工拖动顶点时,编辑器按浮点数精度操作,两边坐标只差一点点,肉眼完全看不出来,但空间分析会把这当作不同位置。
解决:打开编辑器的顶点捕捉功能,捕捉容差设到至少 0.000001 度。已经出现缝隙的,把公共边两侧顶点坐标强制统一,或用拓扑修复工具做一次去重。如果编辑器没有捕捉功能,就在编辑前先做一个全图的定点替换,把所有靠近公共边的顶点吸附到同一坐标。
5.5 现象:图层加载报“类型不支持”或“无法读取”
打开别人发来的 SHP 文件,编辑器直接拒绝加载,或者在保存后 ArcGIS 报“不支持的类型”。
原因:文件含三维坐标或特殊形状类型。SHP 不只 1、3、5 三种类型,还有带 Z 值的点(类型 11)、带 Z 值的折线(13)、带 Z 值的多边形(15),以及 MultiPatch(类型 31)。很多老 VC 编辑器只处理二维的 1、3、5,遇到含 Z 值的文件就懵了。
解决:先用支持三维的工具确认文件类型,把 Z 值剥离,转成二维再交给老编辑器。另一个思路是检查文件头第 32 到 35 字节的形状类型,确认不是 11、13、15、31 这些特殊值。如果你的任务不涉及高程,提前转 2D 能省掉大半兼容性问题。
6. 把 SHP 编辑器接进数据流:转 txt、转 3dtiles、和 dwg 互通
小工具单拎出来干不了大事,放进数据流里才有价值。这里说三条最常用的接法。
第一条是从 SHP 导出坐标文本。非 GIS 同事要一份点位的经纬度表格,没必要让他们装软件。写个小函数把顶点坐标落成 CSV,注意保留要素编号:
void ExportShpToTxt(const char *pszBase, const char *pszCsv) { SHPHandle hSHP = SHPOpen(pszBase, "rb"); if (!hSHP) return; FILE *fp = fopen(pszCsv, "w"); if (!fp) { SHPClose(hSHP); return; } int nEntities, nType; double adfMin[4], adfMax[4]; SHPGetInfo(hSHP, &nEntities, &nType, adfMin, adfMax); fprintf(fp, "id,x,y\n"); for (int i = 0; i < nEntities; i++) { SHPObject *psObj = SHPReadObject(hSHP, i); if (!psObj) continue; for (int j = 0; j < psObj->nVertices; j++) { fprintf(fp, "%d,%.8f,%.8f\n", psObj->nShapeId, psObj->padfX[j], psObj->padfY[j]); } SHPDestroyObject(psObj); } fclose(fp); SHPClose(hSHP); }输出的小数位数建议保留 8 位,不要贪多。坐标精度写多了文件体积成倍涨,写少了叠加其他数据时偏移明显。
第二条是把县域边界、社区地块这类 SHP 转成 3dtiles 前的几何清理。shp转3dtiles的场景里,最怕的不是格式转换,而是转换前的数据脏。编辑器在这里要做三件事:清除重复顶点、确保每个面闭合、把多部件要素拆成独立面。县城边界 SHP 这类大范围数据,拓扑修完再转,瓦片切割时才不会出现缺口。
第三条是 dwg 转 shp 的交接。SHP 编辑器打不开 DWG,但可以处理转换后的产物。常见做法是先在 CAD 里把图层整理好,导出 DXF,再用 QGIS 或专用转换工具转成 SHP。编辑器在整个链条里负责最后一公里:把转换导致的断线、不闭合面、属性乱码修干净。反过来从 SHP 转 dwg 也一样,先用编辑器整理几何和属性,再转 CAD,不然碎线一堆,CAD 里根本没法标注。
我个人的习惯是给编辑器配一个同目录的“验证脚本”,每次改完自动跑三查:几何数量对齐、边界范围重算、字段长度检查。这套流程用过几年,救回过不少临交付前才发现的低级问题。工具虽老,思路不老,希望帮到你。
本文还有配套的精品资源,点击获取