简介一份基于Visual C开发的Shapefile矢量编辑工具源码包面向GIS开发者、测绘数据处理人员及有矢量编辑需求的编程学习者。Shapefile是GIS领域广泛使用的地理数据格式该工具覆盖点、线、多边形等要素的创建、修改、删除及属性编辑等常见操作可应用于地图数据整理、空间要素调整和格式预处理等场景。包内共167个文件以53个.h头文件和49个.cpp源文件为主体完整保留VC工程文件.dsp/.dsw/.rc等同时包含24个图标、22个位图及少量辅助配置整体压缩包仅1.43MB结构紧凑、便于部署。已有334人下载学习。读者拿到的是可直接编译运行的工程源码既可当作桌面端Shapefile编辑器使用也能透过代码理解矢量数据模型、文件读写和几何操作的实现方式对学习GIS底层编程或进行二次开发颇有参考价值。1. 一个 shp editor 的起点为什么读懂 SHP 文件比会按按钮更重要收到过不少这种压缩包名字里带着 Shp__Editor__x.rar解开是一个用 Visual C 写的 shp 矢量编辑小工程。早期 GIS 圈把 shp 文件和 VC 绑在一起的年代这类小工具满地都是现在回看反而是理解“shp 文件到底是什么”最好的教材。编辑器能不能顺手核心就两件事几何写进 .shp属性写进 .dbf中间靠记录号串起来。搞清楚这两点你在 arcgispro 里做 shp 合并、按字段筛选、甚至往 shp 转 3dtiles 的管道里喂数据之前都能先把数据捏在自己手里。这篇按一个最小可用的 shp editor 来拆从文件结构讲到 VC 工程里能直接编译的读取、写入、裁切、合并代码再把容易翻车的边界条件写透。2. 把 SHP 格式拆开看几何、属性、索引三件套如何决定编辑器的写法2.1 SHP 文件不是“一个文件”是三件套的协同打开任何一个 .shp正常情况下它身边必须躺着 .shx 和 .dbf才能被 ArcGIS、QGIS 完整识别。几何坐标逐条写在 .shp 里第几条记录在文件的什么位置由 .shx 按偏移量索引每个要素的字段值比如 ID、名称、面积存放在 .dbf 里。三条记录流靠一个隐式约定对齐第 N 条几何记录对应 DBF 的第 N 行属性。这对编辑器设计的影响是决定性的。很多第一次拿到 .rar 源码的人在读代码时会困惑为什么读几何是一套函数读属性又得单独开一个 DBF 句柄因为 shapelib 的设计就是这样几何读写走 SHP 接口属性读写走 DBF 接口两边互不认识只靠记录号配对。理解了这个前提后面写增删改逻辑才不会犯“只写了 .shp 忘了 .dbf”的错。文件头本身也有意思。前 4 字节固定是文件码 9994按大端存储第 24 字节开始是 4 个 double 组成的 bounding box记录的是整份文件的坐标范围小端存储。这个 bbox 看起来不起眼却是后面自检脚本的重要依据。编辑器里常见的“取全图范围”“按范围过滤”很多实现根本不去遍历全部顶点直接读文件头就能拿到结果。2.2 记录结构从二进制到几何对象的映射每一条几何记录由 8 字节记录头和一段变长内容组成。记录头里前半部分是记录号从 1 开始大端后半部分是内容长度按 16 位字为单位同样大端。内容则从形状类型开始小端存储后面跟着坐标。以最常见的多边形为例内容依次是形状类型、最小外包矩形、环的数量、每个环的起始顶点下标、每个环的类型、总顶点数、所有顶点的 X 数组和 Y 数组。多部件面比如一个带洞的行政区内部可能是多个 ring外环类型是 SHPP_OUTERRING内环是 SHPP_INNERRING。这个“部件”概念是新手最容易踩的地方——读数据时如果忽略 panPartStart把所有顶点当一个闭合环去算面积结果会错得离谱。删除和插入也不像改文本文件那样能“删一行”。SHP 的记录是变长块我们手里的 shapelib 提供的写入方式都是追加写。要删除中间某条要素正确的路径是读一份、过滤条件、写一份新的而不是在原文件上抠掉一段字节。这就是 shp 编辑器的基本工作模式读源文件、构建内存对象、按业务规则变换、写出新文件。理解了这一点你的编辑器架构从一开始就会往“流水线”方向设计而不是往“文件指针随便挪”方向设计。2.3 编辑器的定位不替代 ArcGIS替代的是重复劳动有人会问编辑 shp 明明用 ArcGIS Pro、QGIS 打开就能做为什么要拿 VC 写一个真实原因是三类场景其一批量任务比如把 200 个 shp 文件统一加个“数据来源”字段手工点一遍要半天脚本或命令行工具几秒钟跑完其二管道集成从 dwg 转 shp、json 转 shp、kml 转 shp 的数据流里中间经常要插一步清洗形状类型不对、bbox 越界、坐标有重复点编辑器程序化处理比手动画可靠其三学习价值shp 是公开格式自己写一套读写逻辑之后你去看 OGR、GDAL 的源码或文档会轻松得多。所以我不会建议你在工程里再写一个比肩 ArcGIS 的编辑器那没有必要。建议的方向是用 shapelib 做底层把“读取—变换—写出”的管道搭稳然后在上面挂你业务里最高频的几个操作。标题里那个 shp_vc其实就是这条路的起点——Visual C 编译一个 C 语言写的 shp 操作库完全可行而且工程结构很轻。3. 用 Visual C 编译 shapelib读取、遍历到写出新文件的骨架代码3.1 为什么选 shapelib而不是从零解析二进制从零解析 SHP 是一个非常“涨知识但别用于生产”的方案。文件头、记录头、大端小端混排、DBF 字段描述符处处是坑写出来能跑但遇到带 Z 值、带 M 值、多部件面、日期字段时就容易崩。shapelib 是一套纯 C 实现的读写库核心文件就几个shpopen.c、dbfopen.c、safileio.c配套 shapelib.h、shapefil.h、dbfopen.h。它在 VC 工程里的编译方式极其简单把 .c 文件加进工程包含头文件路径就能直接调用。在 Visual C 的工程里如果你的项目是 C 工程头文件包含时注意 shapelib 的声明是按 C 兼容写的新版本不需要额外包 extern C直接 include 即可。如果你拿到的是老版本源码编译器报链接错误常见做法是在包含头文件前后加上extern C { }。这里我按当前常见版本的 API 写代码接口命名基本稳定。3.2 读取并遍历一个 shp几何与属性两条路径下面是最小可用的读取骨架。它做的事情是打开 .shp 和 .dbf读取要素总数和文件头 bbox然后逐条把几何对象和属性字段取出来。#include shapefil.h void ReadShp(const char* pszPath) { // 第二参数 rb 表示只读shapelib 从这里拿到几何 SHPHandle hSHP SHPOpen(pszPath, rb); if (hSHP NULL) { return; } int nEntities 0; int nShapeType 0; double adfMinBound[4] { 0.0 }; double adfMaxBound[4] { 0.0 }; // 要素个数、几何类型、文件头 bbox 一次拿全 SHPGetInfo(hSHP, nEntities, nShapeType, adfMinBound, adfMaxBound); // 同名路径再开一个 DBF 句柄属性另走一条路 DBFHandle hDBF DBFOpen(pszPath, rb); if (hDBF NULL) { SHPClose(hSHP); return; } int nFields DBFGetFieldCount(hDBF); for (int i 0; i nEntities; i) { SHPObject* psObj SHPReadObject(hSHP, i); // 如果是面顺手用鞋带公式算面积顺便暴露环方向问题 double dArea 0.0; if (nShapeType SHPT_POLYGON) { for (int j 0; j psObj-nVertices - 1; j) { dArea psObj-padfX[j] * psObj-padfY[j 1] - psObj-padfX[j 1] * psObj-padfY[j]; } dArea * 0.5; } // 属性在另一个句柄里按同一记录号取 for (int f 0; f nFields; f) { const char* pszVal DBFReadStringAttribute(hDBF, i, f); // 这里按业务需要存入你的结构体 } SHPDestroyObject(psObj); } DBFClose(hDBF); SHPClose(hSHP); }这段代码的逻辑重点有三个。第一SHPGetInfo拿到的 nShapeType 是文件级形状类型整个文件只能有一种几何类型如果一个数据里混着点和线那它从一开始就不符合 SHP 规范。第二SHPReadObject返回的 SHPObject 内部包含顶点数组、部件数组、类型字段用完必须SHPDestroyObject不然内存泄漏。第三几何和属性各开各的句柄记录号 i 是两边唯一的关联键任何“我只要几何不要属性”的操作都要警惕写出后 dbf 与 shp 记录数对不齐。3.3 写出一个最简单的面SHP 和 DBF 同步创建写入的骨架比读取多一个步骤要先建两个句柄再写几何、写属性最后关闭。这里给一个创建单面要素的完整例子。#include shapefil.h int WriteShp(const char* pszOutPath) { // wb 表示新建覆盖对 SHP 和 DBF 都要执行 SHPHandle hSHP SHPOpen(pszOutPath, wb); DBFHandle hDBF DBFCreate(pszOutPath); if (hSHP NULL || hDBF NULL) { return -1; } // 文件级形状类型必须在此刻确定后续写入对象类型必须一致 SHPCreate(hSHP, SHPT_POLYGON); // 字段类型常量FTInteger 整形、FTString 字符串、FTDouble 浮点 DBFAddField(hDBF, ID, FTInteger, 10, 0); DBFAddField(hDBF, NAME, FTString, 50, 0); // 一个三角形闭合环最后一个顶点与第一个顶点重合 double adfX[4] { 100.0, 200.0, 150.0, 100.0 }; double adfY[4] { 100.0, 100.0, 180.0, 100.0 }; SHPObject* psObj SHPCreateSimpleObject( SHPT_POLYGON, 4, adfX, adfY, NULL); // nShapeId 传 -1 表示追加写返回的是记录号从 0 开始 int nId SHPWriteObject(hSHP, -1, psObj); if (nId 0) { // 这里 nId 直接当 dbf 的行号用两边天然对齐 DBFWriteIntegerAttribute(hDBF, nId, 0, 1); DBFWriteStringAttribute(hDBF, nId, 1, demo); } SHPDestroyObject(psObj); DBFClose(hDBF); SHPClose(hSHP); return nId; }关键参数说明SHPCreate决定文件级别的几何类型一旦建立后面每个对象类型都要一致写混了打开就报错DBFAddField的字段名最长 10 个字符超出会被截断这是 DBF 格式本身的上限SHPCreateSimpleObject的第三个参数是顶点个数多边形必须首尾闭合不闭合的话 ArcGIS 能画出来但几何检查会报错。SHPWriteObject返回的记录号同时也是 DBF 的行号这个返回值要接住后面写属性全靠它。3.4 字段类型与日期字段的隐蔽问题shapelib 的 DBF 字段类型有五种FTString、FTInteger、FTDouble、FTLogical、FTDate。很多人写日期字段时直接调DBFWriteStringAttribute往里写 2024-05-01打开后字段显示正常但类型已经是字符串后续在 ArcGIS 里做时间筛选会失效。常用做法是创建字段时用 FTDate写入时调DBFWriteDateAttribute(hDBF, nId, f, 2024, 5, 1)它内部按 DBF 的 YYYYMMDD 格式存。如果你拿到的源数据是从别的软件转出来的日期字段其实是字符串转换时先DBFGetFieldInfo看类型再决定走哪条写入函数。还有一个常见场景是 shp 转 txt 后再导入编辑器。txt 读回来时所有字段都变成字符串你写回 dbf 时如果不恢复字段类型下游做统计的同事会骂人的。我的习惯是任何导入流程都保留一份“字段类型映射表”导出导入共用避免来回转换后类型信息丢失。4. 按真实业务写编辑逻辑删选、合并、矩形裁切与渔网分割的实现次序4.1 条件筛选与删除重写永远比原地删安全删除要素的常规方案不是从原文件里抠记录而是把符合条件的对象写进新文件。这也是 shp editor 里最稳的一个操作模型。下面这段代码按面积阈值过滤面要素并把字段结构复制过去。#include shapefil.h void FilterByArea(const char* pszIn, const char* pszOut, double dMinArea) { SHPHandle hIn SHPOpen(pszIn, rb); DBFHandle dIn DBFOpen(pszIn, rb); SHPHandle hOut SHPOpen(pszOut, wb); DBFHandle dOut DBFCreate(pszOut); if (!hIn || !dIn || !hOut || !dOut) { return; } int nEntities 0, nType 0; double bMin[4] { 0 }, bMax[4] { 0 }; SHPGetInfo(hIn, nEntities, nType, bMin, bMax); SHPCreate(hOut, nType); // 复制字段结构遍历源 dbf 的字段描述逐个重建 int nFields DBFGetFieldCount(dIn); for (int f 0; f nFields; f) { char szName[12] { 0 }; int nWidth 0, nDecimals 0; DBFFieldType eType DBFGetFieldInfo(dIn, f, szName, nWidth, nDecimals); DBFAddField(dOut, szName, eType, nWidth, nDecimals); } for (int i 0; i nEntities; i) { SHPObject* psObj SHPReadObject(hIn, i); double dArea 0.0; if (nType SHPT_POLYGON) { for (int j 0; j psObj-nVertices - 1; j) dArea psObj-padfX[j] * psObj-padfY[j 1] - psObj-padfX[j 1] * psObj-padfY[j]; dArea * 0.5; if (dArea 0) dArea -dArea; } if (dArea dMinArea) { int nNewId SHPWriteObject(hOut, -1, psObj); // 按字段类型分别复制避免类型错乱 for (int f 0; f nFields; f) { DBFFieldType eType DBFGetFieldInfo(dIn, f, NULL, NULL, NULL); if (eType FTInteger) DBFWriteIntegerAttribute(dOut, nNewId, f, DBFReadIntegerAttribute(dIn, i, f)); else if (eType FTDouble) DBFWriteDoubleAttribute(dOut, nNewId, f, DBFReadDoubleAttribute(dIn, i, f)); else DBFWriteStringAttribute(dOut, nNewId, f, DBFReadStringAttribute(dIn, i, f)); } } SHPDestroyObject(psObj); } DBFClose(dOut); DBFClose(dIn); SHPClose(hOut); SHPClose(hIn); }这段代码解决了字段复制中 80% 的坑不能直接拿一个void*无脑写要区分整型、浮点、字符串走不同函数。你从 json 转 shp、kml 转 shp 这类流程里拿到的中间结果字段几乎全是字符串经这里跑一遍再转成目标类型比在 ArcGIS 里手工改字段类型快得多也比 ArcGIS Pro 里“按属性选择 导出”的重复操作适合自动化。4.2 把多个面合并成一个要素部件数组的正确组装方式合并操作要区分两个层次几何上真的把两个面融成一个外边界求并集和“只是把多个面装进一个多部件要素”。后者写起来简单也是很多“arcgispro shp 合并”需求的底层逻辑。要做多部件面核心是把每个源对象变成一个 ring然后把所有 verts 按顺序塞进一个大对象的数组。#include shapefil.h #include cstring SHPObject* MergeToMultiPart(SHPObject** ppsObjs, int nCount) { // 第一步统计总顶点数后面要一次性分配数组 int nTotalVerts 0; for (int i 0; i nCount; i) nTotalVerts ppsObjs[i]-nVertices; // 先建一个空对象再手动填充 parts 和顶点 SHPObject* psNew SHPCreateObject( SHPT_POLYGON, -1, nCount, NULL, NULL, nTotalVerts, NULL, NULL, NULL, NULL); int nVertBase 0; for (int i 0; i nCount; i) { SHPObject* psSrc ppsObjs[i]; // part 的起点是前一个 part 的累计顶点数 psNew-panPartStart[i] nVertBase; // 区分外环和内环简单合并场景默认全是外环 psNew-panPartType[i] SHPP_OUTERRING; std::memcpy(psNew-padfX nVertBase, psSrc-padfX, psSrc-nVertices * sizeof(double)); std::memcpy(psNew-padfY nVertBase, psSrc-padfY, psSrc-nVertices * sizeof(double)); nVertBase psSrc-nVertices; } psNew-nVertices nVertBase; return psNew; }参数说明里最值得记住的是panPartStart和panPartType这两个数组的行为panPartStart存每个 ring 第一个顶点在全局顶点数组里的下标panPartType标记是外环还是内环。很多人在这一步翻车合并出来的文件在 QGIS 里显示成一个巨大多边形缩放时图形乱飞多半是因为 part 下标写错导致顶点被串成一个环。SHPP_OUTERRING和SHPP_INNERRING是 shapelib 预定义的常量写内环时必须显式指定编辑器不会自动判断洞的方向。如果你是做行政区合并这类真实业务还要注意相邻面合并后会出现共边、飞点、自相交。多部件合并只是“装进一个箱子里”不做拓扑清理。真正的几何融合需要求外包或做布尔并集那是另一个量级的工作别指望几行代码搞定。4.3 矩形裁切与渔网分割一个能直接用的裁剪实现“如何在 shp 图中去掉一部分矢量”是出现频率极高的问题。常见做法有两种按字段筛选掉落在范围内的要素或者对单个要素做几何裁切。后者需要实现多边形裁剪。这里给一个支持矩形裁剪窗口的最小实现思路是标准的 Sutherland–Hodgman 算法裁四边。struct Pt { double x, y; }; // 对多边形顶点序列用一条有向边裁剪 static void ClipEdge(std::vectorPt in, const Pt a, const Pt b, bool inside(const Pt p), Pt inter(const Pt p, const Pt q)) { std::vectorPt out; int n (int)in.size(); for (int i 0; i n; i) { Pt cur in[i]; Pt nxt in[(i 1) % n]; bool curIn inside(cur); bool nxtIn inside(nxt); if (curIn nxtIn) { out.push_back(nxt); } else if (curIn !nxtIn) { out.push_back(inter(cur, nxt)); } else if (!curIn nxtIn) { out.push_back(inter(cur, nxt)); out.push_back(nxt); } // 两个点都在外部时什么都不加 } in.swap(out); } std::vectorPt ClipRect(const std::vectorPt poly, double xmin, double ymin, double xmax, double ymax) { std::vectorPt cur poly; // 四条边依次裁左、右、下、上 ClipEdge(cur, { xmin, ymin }, { xmin, ymax }, [](const Pt p) { return p.x xmin; }, [](const Pt p, const Pt q) { double t (xmin - p.x) / (q.x - p.x); return Pt{ xmin, p.y t * (q.y - p.y) }; }); // 右侧、下侧、上侧同理这里省略重复函数体 return cur; }生产环境里这种手写裁切大多数时间不如直接用 GDAL 的 OGRGeometry 来得快。但把它放在 shp editor 里有一层价值你可以在 UI 上画一个矩形框实时预览裁切结果不需要依赖外部命令行。而渔网分割 shp 这种需求原理就是先生成一组网格矩形再把每个网格按上述算法裁切面要素如果你要的就是带网格属性的新 shp这套东西正好接上。注意裁剪结果没做拓扑重建一个面被切成两块后是两个碎片你要在写出前判断碎片面积、去除过小的残片。5. 避坑shp 编辑的 5 个高频翻车现场与验证手段5.1 写出后中文属性变成乱码现象代码里写进去的中文用 ArcGIS 打开是乱码或者反过来ArcGIS 里正常的中文用 shapelib 读出来是乱码。原因DBF 文件的编码没有强制规范。老数据常见 GBK/GB2312新软件默认按 UTF-8 猜两边不一致就乱了。而 shapelib 本身不处理编码它只负责把字节原样读写。解决统一约定。我的习惯是工程里新增一个编码转换层写入前把 UTF-8 转成 GBK读取后把 GBK 转回 UTF-8。如果项目完全在较新环境中使用也可以全程 UTF-8但要保证下游软件版本支持。设一个明确的“源编码”字段每个数据入口都记录编码来源比事后猜省事得多。5.2 字段名超过 10 个字符被静默截断现象字段名写得很长回头看数据库里名字少了半截下游脚本按全名取值取不到。原因DBF 格式头部的字段描述符里字段名区域只有 11 字节实际可用 10 个字符加一个终止符。shapelib 的DBFAddField不报错直接截断。解决建表前做一步字段名规范化。缩写规则固定比如administrative_division写成AD_DIV并维护一张全名到缩写映射的对照表写到数据字典里。这个坑在从 json 转 shp、dwg 转 shp 的管道里特别常见因为源头字段名往往很长。顺带说一句网上流传的 header editor 这类小工具主要功能就是改字段名和类型你如果手里数据已经写坏了可以用它抢救但不建议把这类工具当成常规环节。5.3 多边形没闭合几何校验一片红现象文件打开正常但 QGIS 的“检查几何”或 ArcGIS Pro 的“修复几何”报一堆“不闭合”错误。原因写入时顶点数组首尾坐标不一致。很多从 CAD 转过来的数据矩形的第四个点和第一个点坐标相同但漏了最后一遍生成的就是三个点的不闭合环。SHPCreateSimpleObject对顶点数没有任何校验三个点也能写进去。解决写自己的EnsureClosed工具函数遍历每个 part比较首尾顶点坐标不相等就追加一个和首点相同的顶点。注意 nVertices 也要加 1。另外环的方向也很重要外环应逆时针内环应顺时针虽然大部分软件能容忍反方向但做 shp 转 3dtiles 这类对拓扑敏感的流程时方向错误可能导致三角网上下颠倒。5.4 只写了 shpdbf 没同步记录数对不上现象用 OGR 或 ArcGIS 打开后属性表比几何少几条或者多几条部分要素“没有属性”。原因几何和属性是在两套句柄里分别写入的代码里如果SHPWriteObject和DBFWrite*没有一一对应执行两侧记录数就会错位。最常见的写法是几何写完后忘了写属性或者是按条件 skip 了几何但没 skip 属性。解决把“写一条要素”封装成一个函数同时接受几何和属性数组内部先写几何拿到 nId再用 nId 写属性任何一步失败就整体回滚并记日志。不要在两个地方维护同一套循环。写完数据后用SHPGetInfo和DBFGetRecordCount对一下两边数量并用一个小函数逐条校验这是成本最低的后置防线。5.5 bbox 没更新文件头与实际坐标范围不一致现象文件头 bbox 是全零或明显小于实际坐标范围缩放到全图时图形“跑出视野”。原因如果你用二进制方式改写了记录内容但没有重算文件头的 bbox或者SHPCreate后直接手动构造记录块而不是走SHPWriteObject头信息就不会自动更新。shapelib 在正常写入路径下会自动维护 bbox但当你用低级方式拼接数据时它帮不了你。解决依赖SHPWriteObject写数据并在写完所有要素后调用一次SHPClose让它落盘。如果做了手工修改用下一章的自检函数扫描一遍实际坐标范围与文件头的 bbox 比较误差超过 1e-6 就该重新计算并重写文件头。这个检查我平时都会放在测试脚本里尤其是从 CAD 转 shp 的数据因为 CAD 文件里经常存在超出常规坐标范围的散点。6. 写一个自检函数不打开 ArcGIS 也能验几何和 bbox写完 shp 后的第一件事不是双击打开而是跑一段自检代码。我的习惯是维护一个ValidateShp函数读一遍刚写出的文件检查四件事几何数量与 dbf 记录数是否一致每个多边形环是否闭合外环面积符号是否与预期一致所有顶点的实际坐标范围与文件头 bbox 是否吻合字段名是否与建表时一致、有没有被截断。实际坐标范围检查用SHPGetInfo拿到的 bbox配合遍历每个对象求出 min/max。两者差值超过一个很小的容差就输出警告。闭合检查则遍历每个 part比较panPartStart对应的第一个顶点和该 part 最后一个顶点。把这些检查并进编译产物的一个命令行参数比如editor --validate out.shp每次生成数据后跑一遍比在 ArcGIS 里肉眼缩放可靠得多。遇到自检不过的文件我会先用 010 editor 这类十六进制工具直接看文件头第 24 字节到第 40 字节是 bbox 的四个 double第 24 到第 28 字节如果读出来的值和实际坐标差得远基本就是头没更新。这时候最省事的后悔药是重新走一遍“读源文件、写新文件”的流程而不是手工改字节。这套编辑器骨架我前后改过很多轮最后留下的教训只有一条shp 编辑器的复杂度 80% 不在文件读写而在“数据的完整性假设”。你没法假设源文件必定闭合、必有投影、必无双值字段所以自检函数要写进发布流程而不是等用户报 bug。希望这篇能把你在 shp 编辑方向上的第一步铺平少走我当时踩过的那些弯路。本文还有配套的精品资源点击获取