☰
C++手写Shapefile读写层:MFC矢量图形模块实战
2026/10/26 19:38:11 网站建设 项目流程

简介:这是一份面向具备C++基础、希望进入GIS图形开发领域的Windows开发者与课程学习者的MFC实战工程,兼顾Shapefile格式原理理解与桌面查看器实践,围绕二进制读写、几何对象绘制及视图交互展开。压缩包共62个文件,约22.64MB,以cpp与h源码、bmp位图及ico图标资源为主,辅以vcxproj工程文件、rc资源脚本和sln解决方案,可直接用Visual Studio打开研究项目结构与编译配置。目前已有1872人学习下载。工程涵盖.shp与.dbf解析、CShape数据结构封装、OnDraw绘制、滚轮缩放与鼠标平移、CDialog文件选择等模块,便于读者理解Esri Shapefile的多文件组成、记录头与坐标数据细节,也能对照源码搭建可缩放、可平移的轻量级查看器;配套主框架、文档视图、资源图标与工程配置形成完整结构,适合作为GIS桌面程序开发入门、二次开发参考及调试经验积累。

1. 从 MFC 里打开一个 shapefile:为什么我最后选择手写读写层

去年给一个 MFC 桌面端做图纸管理模块,对方丢过来一个压缩包,解压后是同名的.shp、.shx、.dbf、.prj四个文件,要求很朴素:程序里能直接打开、平移缩放、点选要素、改两个属性字段再存回去。第一反应当然是挂一个成熟的矢量库,但工程是 VS 的老项目,字符集、运行时库、编译选项一堆历史包袱,引入外部依赖的代价比想象中大。折腾两天之后我换了思路:shapefile 的格式本身足够简单,C++ 手写一个读写层大概八百到一千行,无依赖、可控、出问题能单步调试进去。这篇就按「二进制骨架 → 读取解析 → 写回编辑 → 排查踩坑 → MFC 视图落地」的顺序,把这套东西拆一遍,适合已经会 C++、正在 MFC 里做图形模块、被第三方库的编译问题劝退过的人。

2. 拆开二进制骨架:100 字节文件头、两套字节序与 shx 偏移算法

动手写代码之前,得先把文件结构在纸上画清楚。shapefile 不是「一个文件」,而是一组必须同名的文件,其中三个是强制的。很多人第一次读它,会在同一个 100 字节的头里遇到两种字节序,然后卡住半天——这是这个格式最反直觉的地方,也是后面所有解析代码的基础。

2.1 五个同名文件各管什么

先把分工列清楚,后面写代码时才知道哪些必须同步、哪些可以丢:

后缀是否必需作用长度特征
.shp必需几何主文件,存要素的坐标与形状类型变长,单文件建议不超过 2GB
.shx必需定长索引,每条记录 8 字节,指向.shp里的位置100 字节头 + 8×要素数
.dbf必需属性表,定长记录,和.shp按顺序一一对应头 + 32×字段数 + 记录长×要素数
.prj可选坐标系描述,纯文本 WKT几百字节
.cpg可选属性表用的代码页,纯文本十几个字节

关键约束是「按顺序一一对应」:.shp里第 N 条记录,就对应.dbf里第 N 条记录,中间没有 ID 字段做关联。所以任何删除要素的操作,都必须同时改三个文件,做不到只改一个。.shx的存在意义是让「跳到第 N 个要素」从遍历变成一次寻址,几万个要素时差别很明显,所以它不能省。

2.2 100 字节文件头:两套字节序并存的怪结构

.shp和.shx的头都是 100 字节,字段布局一致,只是后半段的值不同。麻烦在于:前 24 字节按大端存,从第 28 字节开始按小端存。同一个结构里两套字节序,这在老格式里不算罕见(历史包袱),但如果你习惯性写一个ReadStruct一把梭,读出来必然是乱码。

偏移长度字节序含义
04大端 int32文件标识,固定 9994
420—保留,全 0
244大端 int32文件长度,单位是16 位字(×2 才是字节)
284小端 int32版本号,固定 1000
324小端 int32形状类型,1=点 3=线 5=面 8=多点 0=空
3632小端 double×4Xmin、Ymin、Xmax、Ymax
6832小端 double×4Zmin、Zmax、Mmin、Mmax(二维数据全 0)

读头的代码没什么技巧,就是按偏移逐字段处理,大端的走一次字节交换,小端的直接memcpy:

#include <cstdint> #include <cstring> #include <fstream> struct SHPHeader { int32_t fileCode = 0; // 9994 int32_t fileLengthWords = 0; // 单位:16 位字 int32_t version = 0; // 1000 int32_t shapeType = 0; double box[4] = { 0, 0, 0, 0 }; // xmin ymin xmax ymax }; static inline uint32_t Swap32(uint32_t v) { return ((v & 0x000000FFu) << 24) | ((v & 0x0000FF00u) << 8) | ((v & 0x00FF0000u) >> 8) | ((v & 0xFF000000u) >> 24); } bool ReadSHPHeader(std::ifstream& fs, SHPHeader& h) { unsigned char buf[100]; fs.seekg(0, std::ios::beg); if (!fs.read(reinterpret_cast<char*>(buf), 100)) return false; uint32_t u32 = 0; std::memcpy(&u32, buf + 0, 4); h.fileCode = (int32_t)Swap32(u32); // 大端 std::memcpy(&u32, buf + 24, 4); h.fileLengthWords = (int32_t)Swap32(u32); // 大端 std::memcpy(&u32, buf + 28, 4); h.version = (int32_t)u32; // 小端,直接取 std::memcpy(&u32, buf + 32, 4); h.shapeType = (int32_t)u32; // 小端 for (int i = 0; i < 4; ++i) // 小端 double,x86/ARM 上直拷即可 std::memcpy(&h.box[i], buf + 36 + i * 8, 8); // 三个断言能挡掉九成的坏文件 return h.fileCode == 9994 && h.version == 1000 && (h.shapeType == 0 || h.shapeType == 1 || h.shapeType == 3 || h.shapeType == 5 || h.shapeType == 8); }

参数上有两个点值得单独记:fileLengthWords的单位是 16 位字,实际字节数是它的两倍,这个坑后面还会以别的形式出现;shapeType是文件级属性,一个 shapefile 里所有要素必须是同一种类型,想混着存点和面,只能拆成多个文件。至于box,那是全文件的包围盒,可以用来做初始视图的自动缩放,但不要拿它做精确判断——某些工具导出的 bbox 会偏。

提示:如果目标平台不是小端,memcpy读 double 的那一段要再套一层 64 位字节交换,否则box会读成天文数字。

2.3 shx 的偏移算法:单位是 16 位字,不是字节

.shx的结构简单到只有两段:100 字节头和紧随其后的一串 8 字节记录,第 i 条对应.shp的第 i 个要素:

  • 前 4 字节:该要素在.shp里的起始偏移,大端 int32,单位同样是 16 位字;
  • 后 4 字节:该要素的内容长度,大端 int32,单位同样是 16 位字,不含 8 字节的记录头。

所以「第 i 个要素在.shp里的字节位置」=offset × 2,「内容字节数」=contentLength × 2。加载索引就是一次批量读:

struct SHPRecordIndex { int64_t offsetBytes; int32_t contentBytes; }; bool LoadSHXIndex(const std::string& shxPath, std::vector<SHPRecordIndex>& out) { std::ifstream fs(shxPath, std::ios::binary); if (!fs) return false; unsigned char hdr[100]; if (!fs.read(reinterpret_cast<char*>(hdr), 100)) return false; uint32_t u32 = 0; std::memcpy(&u32, hdr + 24, 4); int64_t fileBytes = (int64_t)Swap32(u32) * 2; // 16 位字 → 字节 int64_t recCount = (fileBytes - 100) / 8; // 每条索引固定 8 字节 out.resize((size_t)recCount); for (int64_t i = 0; i < recCount; ++i) { unsigned char r[8]; if (!fs.read(reinterpret_cast<char*>(r), 8)) return false; std::memcpy(&u32, r + 0, 4); out[(size_t)i].offsetBytes = (int64_t)Swap32(u32) * 2; std::memcpy(&u32, r + 4, 4); out[(size_t)i].contentBytes = (int32_t)Swap32(u32) * 2; } return true; }

这段代码看着平平无奇,但它决定了后面随机访问的效率:有了它,点选要素、按属性筛选后回读几何,都是seekg一次的事,不用从头扫。另一种做法是完全不读.shx,顺序遍历.shp,在小数据量下也没问题,但一旦要素上万、又需要频繁按索引取几何,顺序遍历就会变成性能瓶颈。

3. C++ 读取层:把 .shp 记录解成可绘制的几何对象

骨架清楚了,接下来是把字节变成对象。这一层的设计好坏,直接决定 MFC 那边的绘制代码写起来是舒服还是难受。我自己在这块返工过一次,原因就是一开始图省事,把坐标存成了CPoint。

3.1 存储层别用 CPoint:几何坐标的内存模型

CPoint内部是LONG,整数。经纬度或者高斯投影坐标的小数部分会被直接截断,几万米的坐标截断后,缩放放大到一定级别,线就变成锯齿状的了。更麻烦的是 GDI 的坐标也是整数,如果存储层用整数,你会在「精度不够」和「变换后再取整」之间反复纠结。

正确做法是存储层全用double,只在最后一步上屏时才转成int:

enum SHPShapeType { SHAPE_NULL = 0, SHAPE_POINT = 1, SHAPE_POLYLINE = 3, SHAPE_POLYGON = 5, SHAPE_MULTIPOINT = 8 }; struct SHPGeometry { int32_t shapeType = SHAPE_NULL; std::vector<double> xs, ys; // 扁平存放,parts 用下标切 std::vector<int32_t> parts; // 每个 part 在 xs/ys 里的起始下标 double box[4] = { 0, 0, 0, 0 }; int32_t PartCount() const { return (int32_t)parts.size(); } int32_t PointCount() const { return (int32_t)xs.size(); } int32_t PartEnd(int32_t p) const { return (p + 1 < (int32_t)parts.size()) ? parts[p + 1] : PointCount(); } }; struct SHPFeature { int32_t recordNo = 0; // 从 1 开始 SHPGeometry geom; std::vector<std::string> attrs; // 与 dbf 字段顺序一一对应 bool deleted = false; // dbf 里的删除标记 };

用两个平行的vector<double>而不是vector<POINT2D>,看着不直观,但换来两个好处:一是坐标变换时可以直接整块遍历、缓存友好;二是和文件里的存储布局一致,读进来时连复制都省了。parts用下标而不是存子数组,避免了每个 part 一次小内存分配,几万个要素的线数据下,这个差别是实打实的。

3.2 主文件遍历:记录头 + 按类型分派

.shp的记录结构是「8 字节记录头 + 变长内容」。记录头里两个字段都是大端:记录号(从 1 开始,不一定是连续的,但通常连续)和内容长度(16 位字)。内容的前 4 字节是小端的shapeType,然后按类型走不同的解析分支:

  • Point(1):内容就 20 字节,shapeType4 字节 + X、Y 两个 double,没有包围盒。
  • PolyLine(3)/ Polygon(5):shapeType之后是 32 字节包围盒,然后是numParts、numPoints两个 int32,接着numParts个 int32 的 parts 数组,最后numPoints个点。
  • MultiPoint(8):包围盒 +numPoints+ 点数组,没有 parts。
static inline int32_t ReadLE32(const unsigned char* p) { int32_t v; std::memcpy(&v, p, 4); return v; } static inline double ReadLEDbl(const unsigned char* p) { double v; std::memcpy(&v, p, 8); return v; } bool ParseRecordBody(const std::vector<unsigned char>& body, SHPGeometry& g) { if (body.size() < 4) return false; g.shapeType = ReadLE32(body.data()); const unsigned char* p = body.data() + 4; size_t remain = body.size() - 4; if (g.shapeType == SHAPE_POINT) { if (remain < 16) return false; g.xs.push_back(ReadLEDbl(p)); g.ys.push_back(ReadLEDbl(p + 8)); g.parts.push_back(0); g.box[0] = g.box[2] = g.xs[0]; g.box[1] = g.box[3] = g.ys[0]; return true; } if (g.shapeType == SHAPE_POLYLINE || g.shapeType == SHAPE_POLYGON) { if (remain < 40) return false; for (int i = 0; i < 4; ++i) g.box[i] = ReadLEDbl(p + i * 8); int32_t numParts = ReadLE32(p + 32); int32_t numPoints = ReadLE32(p + 36); if (numParts < 0 || numPoints < 0) return false; const unsigned char* q = p + 40; // 长度自检:40 + 4*numParts + 16*numPoints 必须不超过 body 长度 if (40 + 4 * (size_t)numParts + 16 * (size_t)numPoints > remain) return false; g.parts.resize(numParts); for (int32_t i = 0; i < numParts; ++i) g.parts[i] = ReadLE32(q + i * 4); q += 4 * numParts; g.xs.resize(numPoints); g.ys.resize(numPoints); for (int32_t i = 0; i < numPoints; ++i) { g.xs[i] = ReadLEDbl(q + i * 16); g.ys[i] = ReadLEDbl(q + i * 16 + 8); } return true; } return false; // 其余类型按需扩展 }

这段里最重要的不是解析逻辑,而是那行「长度自检」。文件是被外部工具产出的,长度字段和实际内容对不上的情况并不罕见(尤其是被某些脚本手工拼过的数据),不做校验就会越界读。自检失败时不要硬扛,直接把这个文件判为损坏,比读到一半崩掉好排查得多。

另外有一个实战细节:Polygon 的环方向。规范要求外环顺时针、内环(洞)逆时针,但实际数据里两种方向都能遇到,靠方向硬判会翻车——某一批数据里,带洞的面被填成了实心,查了一天半才发现是方向判反。稳妥的做法是先按鞋带公式算有符号面积定初判,再用点在环内测试建立包含关系:被奇数个环包住的才是洞。

3.3 dbf 属性表:32 字节字段描述符与定长记录

.dbf是一个老式桌面数据库的表结构,头部布局固定:

偏移长度含义
01版本字节
13最后更新日期 YYMMDD
44记录条数(小端 uint32)
82头部总长度(小端 uint16),第一个字段描述符的起点是 32
102每条记录长度(小端 uint16),含 1 字节删除标记
291代码页标记(中文数据常见 0x4D/0x4E 或 0)
3232×N字段描述符,以 0x0D 结束

每个字段描述符 32 字节:前 11 字节是名字(最多 10 个 ASCII 字符 + 1 字节结束符),第 12 字节是类型(C 字符、N 数值、F 浮点、D 日期、L 逻辑),第 17 字节是长度,第 18 字节是小数位。记录区是定长的:1 字节删除标记(空格=有效,*=已删),后面按字段长度依次排开,字符字段左对齐补空格,数值字段右对齐补空格。

struct DBFField { std::string name; char type; int len; int dec; }; bool ReadDBF(const std::string& path, std::vector<DBFField>& fields, std::vector<std::vector<std::string>>& rows, std::vector<bool>& deleted) { std::ifstream fs(path, std::ios::binary); if (!fs) return false; unsigned char h[32]; if (!fs.read(reinterpret_cast<char*>(h), 32)) return false; uint32_t recCount = 0; std::memcpy(&recCount, h + 4, 4); // 小端 uint16_t headLen = 0, recLen = 0; std::memcpy(&headLen, h + 8, 2); std::memcpy(&recLen, h + 10, 2); // 字段描述符区:从 32 到 headLen - 1,末尾 1 字节是 0x0D 结束符 int fieldCount = (headLen - 32 - 1) / 32; fs.seekg(32, std::ios::beg); fields.clear(); for (int i = 0; i < fieldCount; ++i) { unsigned char d[32]; if (!fs.read(reinterpret_cast<char*>(d), 32)) return false; DBFField f; f.name.assign(reinterpret_cast<char*>(d), strnlen(reinterpret_cast<char*>(d), 11)); f.type = (char)d[11]; f.len = d[16]; f.dec = d[17]; fields.push_back(f); } fs.seekg(headLen, std::ios::beg); rows.assign(recCount, {}); deleted.assign(recCount, false); std::vector<char> rec(recLen); for (uint32_t r = 0; r < recCount; ++r) { if (!fs.read(rec.data(), recLen)) return false; deleted[r] = (rec[0] == '*'); size_t off = 1; rows[r].resize(fieldCount); for (int i = 0; i < fieldCount; ++i) { std::string v(rec.data() + off, fields[i].len); // 去掉右侧填充空格,数值字段还要去左侧 while (!v.empty() && (v.back() == ' ' || v.back() == '\0')) v.pop_back(); size_t s = v.find_first_not_of(' '); rows[r][i] = (s == std::string::npos) ? std::string() : v.substr(s); off += fields[i].len; } } return true; }

参数说明:headLen用来定位记录区起点,不能想当然按32 + 32 × 字段数 + 1算,某些导出工具会在描述符区末尾塞额外的字节;recLen是逐条累加出来的,如果和你按字段长度加起来的值不一致,说明字段描述符读错了,这时候直接报错,别继续往下走。删除标记必须处理,被标记的记录在几何上还在,但业务上已经作废,读进来之后要么过滤掉,要么保留标记让调用方决定。

4. 写回与编辑:两遍扫描、shx 同步与 dbf 定长记录

写比读难,因为读只要应对「别人怎么存的」,写还要保证「别的软件能读」。这一段我踩的坑比读那边多一倍,主要集中在文件头的回填和.shx的同步上。

4.1 为什么写之前必须扫两遍

.shp的头里有两个字段是「全文件级」的:文件总长度和整体包围盒。而这两个值在写第一条记录之前根本不知道。所以写流程只能是两遍:第一遍遍历内存里的要素,累加每条记录的内容长度、合并包围盒;第二遍才真正落盘。

如果数据来源本来就是读进来的(比如打开一个文件、改属性、再另存),第一遍可以直接复用原来的 bbox 和长度——但只在要素几何完全没变的前提下成立。一旦有要素被增删或移动,必须重算,否则新文件的头部信息和实际内容对不上,某些严格的软件会直接拒开。

4.2 shp 与 shx 成对写:一个不能拆的 writer

最省心的做法是把两个流封在一个类里,写一条要素,两边各写一次,不给它们分开的机会:

class SHPWriter { public: bool Open(const std::string& basePath, int32_t shapeType); bool WriteFeature(const SHPGeometry& g); // 几何坐标已经是文件坐标系 bool Close(); // 回填 .shp 头 + 写 .shx 头 private: std::ofstream m_shp, m_shx; int32_t m_type = 0; int64_t m_shpBytes = 100; // 头已占 100 int32_t m_recCount = 0; double m_box[4] = { 1e38, 1e38, -1e38, -1e38 }; }; bool SHPWriter::WriteFeature(const SHPGeometry& g) { // 内容长度 = shapeType(4) + box(32) + numParts(4) + numPoints(4) + parts + points int32_t contentBytes = 4 + 32 + 8 + 4 * (int32_t)g.parts.size() + 16 * (int32_t)g.xs.size(); if (contentBytes % 2 != 0) return false; // 16 位字为单位,必须偶数 // 记录头:记录号 + 内容长度,均大端,长度单位是 16 位字 WriteBE32(m_shp, m_recCount + 1); WriteBE32(m_shp, contentBytes / 2); // shx 索引:偏移(从文件开头算,16 位字)+ 同样的内容长度 WriteBE32(m_shx, (int32_t)(m_shpBytes / 2)); WriteBE32(m_shx, contentBytes / 2); WriteLE32(m_shp, g.shapeType); for (int i = 0; i < 4; ++i) WriteLEDbl(m_shp, g.box[i]); WriteLE32(m_shp, (int32_t)g.parts.size()); WriteLE32(m_shp, (int32_t)g.xs.size()); for (int32_t v : g.parts) WriteLE32(m_shp, v); for (size_t i = 0; i < g.xs.size(); ++i) { WriteLEDbl(m_shp, g.xs[i]); WriteLEDbl(m_shp, g.ys[i]); // 顺便累积包围盒 m_box[0] = std::min(m_box[0], g.xs[i]); m_box[1] = std::min(m_box[1], g.ys[i]); m_box[2] = std::max(m_box[2], g.xs[i]); m_box[3] = std::max(m_box[3], g.ys[i]); } m_shpBytes += 8 + contentBytes; ++m_recCount; return true; }

Close()里要做三件回填,少一件文件就是残的:一是在.shp第 24 字节写入m_shpBytes / 2;二是在第 36 字节写入累积的m_box;三是给.shx写一个完全一样的 100 字节头(文件长度是100 + 8 × 要素数,bbox 与主文件一致)。写完记得flush再关,Windows 上文件句柄没关干净时,另一个进程读到的可能是半截文件。

4.3 dbf 的定长记录决定了你能怎么改

.dbf是定长记录的,这个特性决定了三种编辑策略的边界:

策略适用场景代价
原地改属性只改字段值、不改字段结构最快,一次seekp覆盖一条记录
追加要素只增不删,字段结构不变改头部记录数 + 追加一条记录,.shp/.shx同步追加
全量重写增删改混合、字段增删慢,但状态最干净,几万条以内可接受

原地改属性的实现要点是算出目标记录的字节偏移:headLen + r × recLen + 1 + 前面所有字段长度之和,然后直接seekp写入定长字符串(不足补空格,超长截断)。注意不要往里写变长内容,一条记录写长了,后面全部的记录都会错位,而且这种错误在打开时不一定报,等到读属性才发现全乱了。

4.4 中文与 .cpg:编码问题在写的时候解决

.dbf的字段名只有 11 字节,留 1 字节结束符,实际最多 10 个 ASCII 字符。用中文当字段名,会被截断成半截字节,读出来就是乱码。我的做法是字段名一律用英文或拼音(NAME、XZQHMC),中文只放在字段值里。

字段值的编码取决于文件头第 29 字节的代码页标记,但更可靠的做法是同时输出一个同名.cpg文本文件,内容写UTF-8或936,主流软件会优先认它。MFC 工程里CString是 UTF-16,出入都要转一次:

// CString(UTF-16) → UTF-8,写 dbf 之前调用 std::string ToUtf8(const CString& s) { int n = ::WideCharToMultiByte(CP_UTF8, 0, s, -1, nullptr, 0, nullptr, nullptr); std::string out(n > 0 ? n - 1 : 0, '\0'); if (n > 1) ::WideCharToMultiByte(CP_UTF8, 0, s, -1, &out[0], n, nullptr, nullptr); return out; }

WideCharToMultiByte返回的长度包含结尾的\0,所以构造std::string时要减 1,否则写进 dbf 的字段值末尾会多一个不可见字符,显示时看不出,做字符串比较时全是坑。

5. 排查记录:五类能把人卡一整天的 shapefile 问题

下面这几条都是实际调试中记下来的,现象都很像「程序写错了」,实际原因大多在格式细节上。按「现象 → 原因 → 解决」列一遍,遇到了可以对着查。

5.1 坐标读出来是 6.9e-310 或者一亿多的天文数字

现象:点要素全跑到视图外面,日志里打出来的xmin是一个明显不合常理的大数或极小数。

原因:文件头前 24 字节是大端,从第 28 字节开始是小端,用了同一套memcpy通吃。也可能是fileLengthWords忘了乘 2,导致后续所有seekg位置都偏。

解决:按偏移逐字段处理,大端字段统一走Swap32,小端字段直接memcpy;读完头立刻校验fileCode == 9994、version == 1000、shapeType在合法枚举内。这三个断言能挡掉绝大部分坏文件,也顺带证明了字节序处理是对的。

5.2 第一个要素显示正常,第二个开始整体偏移

现象:单个点没问题,一换成多段线,第二条要素的坐标就开始飘,越往后越离谱,最后直接读到文件尾。

原因:记录头的contentLength单位是 16 位字,不是字节。少乘 2 会少读一半内容,多乘 2 会跳过下一条记录的一半,两种错法表现不同但都很难一眼看出来。

解决:读完记录头立刻contentBytes = contentWords * 2,并用.shx里同一条记录的长度做交叉校验。两个值对不上,说明解析逻辑有问题,不要继续往下读,就地报错比读出一堆错位数据强。

5.3 面要素填色填反,洞被填成实体

现象:带洞的行政区和管线围成的环形区域,画出来洞的位置也被填上了颜色;或者整个面只剩下一个反色的轮廓。

原因:靠环的方向判断内外环。规范里确实写了外环顺时针、内环逆时针,但不同工具导出的数据并不都守规矩,同一批数据里混着两种方向的情况真实存在。

解决:先按鞋带公式算有符号面积做初判,再用点在环内测试建立包含关系——被奇数个环包住的才是洞。绘制时用pDC->SetPolyFillMode(ALTERNATE)让 GDI 按奇偶规则填充,比手工拆分环再逐个填充稳得多。

5.4 中文属性乱码,字段名还少了几个字

现象:属性面板里中文全是问号或者方块,字段名「名称」变成一串诡异的截断字符。

原因:字段描述符的名字段只有 11 字节,实际可用 10 个 ASCII 字符,中文一个字两三字节,超了就被截断。字段值的编码又取决于文件头第 29 字节的代码页标记,没有.cpg时各家的默认值不一样。

解决:字段名用英文或拼音,中文只放值;写文件时同时落一个同名.cpg,内容写UTF-8或936;MFC 里CString进出 dbf 都要过WideCharToMultiByte/MultiByteToWideChar,别偷懒用CStringA直接赋值,那玩意儿的编码取决于工程的字符集设置,换台机器就变。

5.5 别的软件只认出一半要素,或者提示文件损坏

现象:自己的程序里打开完全正常,发给协作方之后,对方只看到前几条要素,或者干脆报文件损坏。

原因:.shx没跟着重建,或者shx里的偏移写成了字节数而不是 16 位字数;也可能是Close()时忘了回填.shp头里的fileLength。自己的读取代码不依赖.shx(顺序遍历),所以本地测不出来。

解决:把.shp和.shx封进同一个 writer,写一条要素两边各写一次,从结构上杜绝不同步;Close()里三件回填(文件长度、bbox、.shx头)写成固定流程,不依赖调用方记得。自测时养成习惯:写完主动用自己写的「纯.shx索引 + 随机访问」路径打开一遍,能提前发现大部分头部回填遗漏。

6. MFC 视图收尾

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

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

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

立即咨询