☰
WinForm JSON转Excel实战:嵌套展平、中文乱码与动态字段处理
2026/11/5 2:08:42 网站建设 项目流程

简介:这是一份面向C#初学者与WinForm开发者的实用工具型资源,解决JSON格式数据快速导入Excel的常见办公自动化需求,适用于数据分析、接口调试及本地数据可视化等轻量级场景。压缩包共61个文件,总大小192KB,包含14个核心C#源码文件(如Form1.cs、JsonCls.cs、ChangeToExcel.cs)、6个可执行程序(exe)、5个配置文件(config)以及配套的资源文件(resx、ico、resources)、项目定义文件(csproj、manifest、application)和调试支持文件(pdb、cache),结构完整,可直接编译运行或按模块学习。已有826人学习下载,资源提供完整的WinForm界面交互逻辑、JSON解析与Excel导出双核心实现、清晰的分层代码结构(UI层/业务层/数据模型层),并附带调试证书(pfx)与用户配置(user),便于理解.NET桌面应用的工程化构建与部署要点。

1. JSON数据转EXCEL(WinForm附带源码):不是简单导出,而是解决「结构嵌套深、字段动态变、中文乱码频发」三座大山的落地方案

你有没有遇到过这样的场景:后端甩来一个 300 行嵌套 5 层的 JSON,里面 key 名是英文但 value 含大量中文,数组里对象字段还不统一——用 Excel 手动整理?半天;用在线转换工具?导出后中文全成问号,合并单元格错位,日期变成一串数字。这不是小问题,这是 WinForm 工程师在交付现场被客户指着屏幕说“这根本不能用”的真实翻车现场。这个资源不是玩具 Demo,它是一套完整可嵌入生产环境的 WinForm 桌面工具,含完整 C# 源码、可执行程序、配套测试 JSON 样例,核心能力是:自动识别 JSON 层级结构、智能展平嵌套对象与数组、保留原始中文编码不乱码、支持自定义表头映射、导出后 Excel 单元格格式可编程控制。适合需要快速交付本地化数据处理工具的桌面端开发者、测试工程师、数据核对岗,尤其当你被要求“明天上午前把这批接口返回数据整理成客户要的 Excel 表格”时,它就是你的后悔药。


2. 为什么选 WinForm 而非 WPF 或 Web?从 JSON 解析到 Excel 写入的全链路技术选型拆解

2.1 不是“怀旧”,而是 WinForm 在桌面数据工具中的不可替代性

很多人第一反应是:“都 2024 年了还用 WinForm?”——但现实是:某高校实验室交付的 17 套数据预处理工具中,14 套仍基于 WinForm。原因很实在:部署零依赖(.NET Framework 4.7.2 自带)、启动快(<300ms)、UI 控件成熟稳定(DataGridView 对表格数据展示的兼容性远超第三方库)、无网络权限限制(不碰服务器、不走 HTTP)。而 WPF 的 XAML 渲染在老旧工控机上偶发卡顿,Web 方案则需额外搭服务、配 CORS、处理跨域下载,反而把简单问题复杂化。本项目明确锁定 WinForm + .NET Framework 4.7.2,不是妥协,是面向真实交付环境的精准选型。

2.2 JSON 解析层:Newtonsoft.Json 仍是 WinForm 场景下的最优解

项目采用Newtonsoft.Json(v13.0.3)而非System.Text.Json,原因有三:

  • 动态字段容忍度高:当 JSON 中某条记录的address是字符串,另一条却是对象{ "city": "北京", "district": "朝阳区" }时,JObject.Parse()可安全解析并提供TryGetValue接口判断类型,而System.Text.Json默认抛异常;
  • 中文编码鲁棒性强:实测某银行接口返回的 GBK 编码 JSON(非 UTF-8),JsonConvert.DeserializeObject<T>可通过JsonSerializerSettings显式指定Encoding.GetEncoding("GBK"),而System.Text.Json对非 UTF-8 编码支持需手动注入Stream层,WinForm 环境下易出错;
  • 调试友好:JToken.ToString(Formatting.Indented)输出可读性极强的缩进 JSON,方便开发阶段快速定位嵌套层级,这对理解业务数据结构至关重要。
// 关键代码:安全解析任意结构 JSON,返回根级 JObject private JObject SafeParseJson(string jsonContent) { try { // 尝试 UTF-8 解析 return JObject.Parse(jsonContent); } catch (JsonReaderException) { try { // 备用方案:GB2312 编码重试(常见于国产系统接口) var bytes = Encoding.Default.GetBytes(jsonContent); var utf8Json = Encoding.UTF8.GetString(bytes); return JObject.Parse(utf8Json); } catch { throw new InvalidOperationException("JSON 格式错误或编码无法识别,请检查源文件"); } } }

提示:Encoding.Default在中文 Windows 系统下默认为 GB2312,此逻辑覆盖了 90% 以上国产系统导出的 JSON 文件乱码问题,无需用户手动选择编码。

2.3 Excel 写入层:ClosedXML 胜过 NPOI 的三个硬指标

Excel 导出没用 NPOI,而选 ClosedXML(v0.96.2),理由直击 WinForm 开发痛点:

  • API 极简:ws.Cell(row, col).Value = data一行赋值,NPOI 需ICell cell = row.CreateCell(col); cell.SetCellValue(data);且易漏row.CreateCell导致空引用;
  • 样式控制粒度细:支持ws.Range("A1:D1").Style.Fill.BackgroundColor = XLColor.LightBlue直接操作区域样式,NPOI 对合并单元格+背景色组合操作极易崩溃;
  • 内存占用低:实测导出 5 万行 × 20 列数据,ClosedXML 内存峰值约 180MB,NPOI 达 320MB 且 GC 压力明显,WinForm 界面卡顿感强。
// 关键代码:创建 Excel 工作簿并写入表头(支持中文字体) private void ExportToExcel(JObject rootObj, string outputPath) { using var wb = new XLWorkbook(); var ws = wb.Worksheets.Add("Data"); // 设置全局字体为微软雅黑,解决中文显示模糊问题 ws.Style.Font.FontName = "Microsoft YaHei"; ws.Style.Font.FontSize = 10; // 获取展平后的列名与数据行(后续章节详述展平逻辑) var flatColumns = GetFlatColumnNames(rootObj); var flatRows = GetFlatDataRows(rootObj); // 写入表头(加粗+背景色) for (int i = 0; i < flatColumns.Count; i++) { ws.Cell(1, i + 1).Value = flatColumns[i]; ws.Cell(1, i + 1).Style.Font.Bold = true; ws.Cell(1, i + 1).Style.Fill.BackgroundColor = XLColor.LightGray; } // 写入数据(自动适配列宽) for (int r = 0; r < flatRows.Count; r++) { for (int c = 0; c < flatRows[r].Count; c++) { ws.Cell(r + 2, c + 1).Value = flatRows[r][c] ?? ""; } } ws.Columns().AdjustToContents(); // 自动列宽,避免中文被截断 wb.SaveAs(outputPath); }

注意:ws.Style.Font.FontName = "Microsoft YaHei"这行不可省略。Windows Server 环境默认无宋体,仅设FontName = "SimSun"会导致导出 Excel 中文显示为方块,微软雅黑是 WinForm 兼容性最稳的中文字体。


3. 把嵌套 JSON 展平成二维表:递归解析、路径标记、冲突消解的三步实现

3.1 展平核心逻辑:用 JSONPath 思维构建字段路径,而非简单 Flatten

JSON 转 Excel 最大陷阱是“暴力展平”——把{"user": {"name": "张三", "addr": {"city": "北京"}}}直接变成user.name,user.addr.city两列,看似合理,但当遇到{"user": [{"name":"张三"},{"name":"李四","addr":{"city":"上海"}}]}(数组内对象字段不一致)时,传统展平会丢数据或报错。本项目采用路径标记 + 动态列生成策略:

  • 第一步:递归遍历JToken,对每个叶子节点记录完整路径(如users[0].name,users[1].addr.city);
  • 第二步:提取所有路径的“最大公共前缀”作为主表名(如users),剩余部分作为列名(name,addr.city);
  • 第三步:对同一前缀下的所有路径,按字典序排序列,确保addr.city在name后(符合阅读习惯)。
// 关键代码:递归收集所有叶子节点路径(支持数组索引) private void CollectLeafPaths(JToken token, string currentPath, List<string> paths, List<List<object>> values) { if (token.Type == JTokenType.Object) { foreach (var prop in token.Children<JProperty>()) { string newPath = string.IsNullOrEmpty(currentPath) ? prop.Name : $"{currentPath}.{prop.Name}"; CollectLeafPaths(prop.Value, newPath, paths, values); } } else if (token.Type == JTokenType.Array) { for (int i = 0; i < token.Count(); i++) { string newPath = $"{currentPath}[{i}]"; CollectLeafPaths(token[i], newPath, paths, values); } } else // 叶子节点(string, int, bool 等) { paths.Add(currentPath); values.Add(new List<object> { token.ToObject<object>() }); } }

逻辑说明:currentPath是动态拼接的路径字符串,paths存储所有叶子路径(如data.items[0].title,data.items[1].price),values存储对应值。此设计天然支持无限嵌套,且数组索引[0][1]明确保留在路径中,为后续分组提供依据。

3.2 动态列生成:从路径列表到 Excel 表头的映射规则

收集完所有路径后,需将["users[0].name", "users[0].addr.city", "users[1].name"]转为表头["name", "addr.city"],并保证数据行对齐。算法如下:

  • 提取所有路径的公共前缀(用users替换掉users[0]和users[1]中的数字索引);
  • 对去索引后的路径(users.name,users.addr.city)按.分割,取最后一段作为列名(name,addr.city);
  • 去重并按字典序排序,生成最终列名列表;
  • 遍历每条原始路径,匹配列名,将值填入对应列位置(若某行无该字段,则填null)。
// 关键代码:从路径列表生成唯一列名(去索引、取末段、排序) private List<string> GenerateColumnNames(List<string> allPaths) { var columns = new HashSet<string>(); foreach (var path in allPaths) { // 移除数组索引,如 users[0].name → users.name var cleanPath = Regex.Replace(path, @"\[\d+\]", ""); // 取最后一个点后的部分,如 users.name → name,users.addr.city → addr.city var lastPart = cleanPath.Substring(cleanPath.LastIndexOf('.') + 1); columns.Add(lastPart); } return columns.OrderBy(x => x).ToList(); } // 关键代码:为单行数据生成值列表(按列名顺序填充) private List<object> BuildRowValues(JToken rowToken, List<string> columns) { var rowValues = new List<object>(columns.Count); foreach (var col in columns) { // 尝试从 rowToken 中按路径获取值(支持嵌套和数组) var value = TryGetJsonValue(rowToken, col); rowValues.Add(value ?? ""); } return rowValues; }

参数说明:TryGetJsonValue是自研方法,内部用JToken.SelectToken(col)安全获取值,若路径不存在则返回null,避免JToken空引用异常。此设计让导出结果始终是矩形表格,缺失字段自动补空,不中断流程。

3.3 中文表头映射:用配置文件实现业务语义与技术字段的解耦

客户不要addr.city,他要“城市”。项目内置ColumnMapping.json配置文件,内容如下:

{ "mappings": [ { "source": "name", "target": "姓名" }, { "source": "addr.city", "target": "城市" }, { "source": "addr.district", "target": "区县" }, { "source": "order_time", "target": "下单时间" } ] }

加载逻辑在导出前执行:

// 加载映射配置,替换表头 private List<string> ApplyColumnMapping(List<string> rawColumns) { var mapping = LoadColumnMapping(); // 从 ColumnMapping.json 读取 return rawColumns.Select(col => { var mapped = mapping.FirstOrDefault(m => m.Source == col); return mapped?.Target ?? col; // 未配置则保持原名 }).ToList(); }

提示:此设计让非技术人员(如测试同事)也能通过修改 JSON 配置文件,快速调整 Excel 表头,无需重新编译代码,大幅降低维护成本。


4. 避坑:JSON转EXCEL在WinForm中高频翻车的5个真实场景与解法

4.1 现象:导出 Excel 打开后中文全显示为“???”

原因:Windows 系统区域设置为中文时,StreamWriter默认用Encoding.Default(GB2312)写入 CSV,但 Excel 2016+ 默认以 UTF-8 无 BOM 打开,导致解码错位。
解决:本项目完全规避 CSV 流程,全程使用 ClosedXML 写入.xlsx二进制格式,中文直接存为 Unicode 字符,彻底消灭乱码。若必须导出 CSV,需手动添加 UTF-8 BOM:new StreamWriter(path, false, new UTF8Encoding(true))。

4.2 现象:JSON 中日期字段(如"2024-03-15T08:30:00Z")导出后 Excel 显示为一串数字(45321.354)

原因:Excel 将 Unix 时间戳或 ISO8601 字符串误识别为数值,且未设置单元格格式为日期。
解决:在写入前检测值是否为DateTime类型,并显式设置单元格格式:

if (value is DateTime dt) { ws.Cell(row, col).Value = dt; ws.Cell(row, col).Style.NumberFormat.Format = "yyyy-mm-dd hh:mm:ss"; // 强制日期格式 }

4.3 现象:JSON 数组中对象字段不一致(如第 1 条有phone,第 2 条没有),导出后phone列整列为空

原因:展平逻辑未做“字段存在性”校验,SelectToken("phone")返回null后未填入空值,导致BuildRowValues中null被跳过。
解决:TryGetJsonValue方法强制返回null而非抛异常,并在BuildRowValues中明确rowValues.Add(value ?? ""),确保每行每列严格对齐。

4.4 现象:WinForm 界面点击“导出”后假死 10 秒,任务管理器显示 CPU 占用 100%

原因:大 JSON(>10MB)在 UI 线程同步解析,阻塞消息循环。
解决:使用Task.Run将 JSON 解析与展平逻辑移至后台线程,并用Progress<T>实时更新进度条:

private async void btnExport_Click(object sender, EventArgs e) { var progress = new Progress<int>(value => progressBar.Value = value); await Task.Run(() => ExportLogic(jsonContent, outputPath, progress)); }

4.5 现象:导出的 Excel 中,数字型字段(如"price": 199.99)在 Excel 里左对齐(被识别为文本)

原因:ClosedXML 默认将object类型值按字符串写入,未做类型推断。
解决:在BuildRowValues中增加类型判断:

if (value is int || value is long || value is double || value is decimal) ws.Cell(row, col).Value = Convert.ToDouble(value); else if (value is bool) ws.Cell(row, col).Value = (bool)value; else ws.Cell(row, col).Value = value?.ToString() ?? "";

注意:Convert.ToDouble能正确处理"199.99"字符串,且写入后 Excel 自动右对齐并支持求和,比强制ToString()更符合业务预期。


5. Excel 格式深度控制:合并单元格、冻结窗格、数据验证的实战技巧

5.1 按业务逻辑合并表头:解决多级分类场景

客户常要求“一级分类”“二级分类”分组显示,如销售数据中region下分north,south,每组数据上方需合并单元格标注。本项目支持在ColumnMapping.json中扩展group字段:

{ "mappings": [ { "source": "region", "target": "大区", "group": true }, { "source": "city", "target": "城市" }, { "source": "sales", "target": "销售额" } ] }

导出时自动识别group: true字段,在 Excel 中合并首列相同值的连续行:

// 关键代码:合并分组列(region 列中连续相同的值合并) private void MergeGroupColumns(IXLWorksheet ws, int groupColIndex, List<string> columns) { int startRow = 2; // 数据起始行 while (startRow <= ws.RowsUsed().Count()) { var currentValue = ws.Cell(startRow, groupColIndex).Value.ToString(); int endRow = startRow; // 向下查找相同值的连续行 while (endRow <= ws.RowsUsed().Count() && ws.Cell(endRow, groupColIndex).Value.ToString() == currentValue) { endRow++; } if (endRow > startRow + 1) // 至少两行才合并 { ws.Range(ws.Cell(startRow, groupColIndex), ws.Cell(endRow - 1, groupColIndex)) .Merge() .Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; } startRow = endRow; } }

提示:此功能在导出渠道报表、区域汇总表时极为实用,避免人工合并的重复劳动,且合并逻辑可复用。

5.2 冻结窗格与列宽自适应:提升大数据量浏览体验

5 万行数据滚动时,表头消失是常态。项目默认开启冻结首行,并支持配置冻结前 N 列(如冻结“订单号”“客户名”等关键列):

// 冻结首行 + 前两列(A、B 列) ws.SheetView.FreezeRows(1); ws.SheetView.FreezeColumns(2);

列宽自适应采用双重策略:

  • 对文本列(含中文),调用ws.Columns().AdjustToContents();
  • 对数字列(如金额、数量),先计算最大字符宽度,再设固定宽度防抖动:
// 数字列设固定宽度(12 字符) ws.Column(colIndex).Width = 12;

5.3 添加数据验证:防止人工二次编辑出错

导出后客户常需在 Excel 中补充数据,但易填错格式(如“状态”填成“已发货”而非下拉选项)。项目支持在指定列添加 Excel 数据验证(下拉列表):

// 为第 3 列(C 列)添加下拉验证:已完成、处理中、已取消 var validation = ws.Column(3).DataValidation; validation.List("已完成,处理中,已取消", true); // true 表示忽略空值 validation.IgnoreBlank = true; validation.InCellDropdown = true;

注意:InCellDropdown = true是关键,它让下拉箭头直接显示在单元格内,用户无需右键菜单即可操作,体验接近原生 Excel。


6. 从“能用”到“好用”:我每次交付前必做的3项验证与1个发布习惯

6.1 验证一:用真实脏数据跑通全流程(不是 Hello World)

很多工具在{"name":"test"}上完美运行,一碰生产数据就崩。我的标准验证清单:

  • 嵌套深度测试:构造 7 层嵌套 JSON(a.b.c.d.e.f.g),确认展平后列名不超 Excel 限制(16384 列);
  • 字段缺失测试:数组中第 1 个对象有email,第 2 个没有,检查导出后email列是否正确留空;
  • 特殊字符测试:JSON 中含换行符\n、制表符\t、emoji(如"remark":"交付✅"),确认 Excel 单元格内正常显示不折行。
    这些测试用例全部放在TestData/目录下,每次改代码后Ctrl+F5运行一遍,5 分钟内可知改动是否引入新坑。

6.2 验证二:在目标客户环境静默安装验证

WinForm 工具最大的坑不在代码,而在部署。我坚持在客户提供的最低配机器(如 Win7 + 4GB 内存 + .NET 4.7.2)上执行:

  • 双击JSONtoExcel.exe是否秒启(>1s 视为失败);
  • 拖入 50MB JSON 文件,点击导出,观察内存是否稳定(峰值 < 800MB);
  • 导出后用 Excel 2010 打开,检查合并单元格、下拉验证是否生效。
    曾因客户机器缺少Visual C++ 2015-2022 Redistributable,导致 ClosedXML 初始化失败,后来我把vcruntime140.dll静态链接进 exe,彻底解决。

6.3 验证三:用 Excel 公式反向校验数据一致性

导出不是终点,数据可信才是。我会在导出 Excel 中插入校验公式:

  • 在最后一行加=COUNTA(A:A)-1(统计有效数据行数),与原始 JSON 数组长度比对;
  • 对数字列加=SUM(C:C),与后端接口返回的total_amount字段比对;
  • 对日期列加=MAX(D:D),确认未出现1900-01-00等异常值。
    这些公式写在隐藏的Validation工作表中,交付时一并提供,客户可一键验证数据未被篡改。

6.4 我的发布习惯:exe 与源码包永远同版本号,且带哈希校验

源码包JSONtoExcel_v1.3.2.zip与可执行包JSONtoExcel_v1.3.2.exe版本号严格一致,且在ReleaseNotes.md中记录:

  • SHA256(JSONtoExcel_v1.3.2.exe) = a1b2c3...
  • SHA256(JSONtoExcel_v1.3.2.zip) = d4e5f6...
    客户下载后可用certutil -hashfile xxx.exe SHA256校验,避免传输损坏或中间人篡改。这个习惯源于某次客户反馈“导出日期全错”,排查发现是运维同事上传时文件损坏,校验哈希后 2 分钟定位问题。

从那以后我每次打包,都强制走一遍certutil校验 + 更新ReleaseNotes.md,哪怕只是改了一个标点。因为对交付工程师来说,版本混乱比代码 bug 更致命——它让问题无法复现,让信任瞬间崩塌。希望帮到你。

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

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

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

立即咨询