7.1 KiB
7.1 KiB
NPOI 2.1 (.NET 3.5) 跨版本 Excel 列印跑版終極解決方案文件
1. 問題背景與根本原因
在使用 NPOI 2.1 配合 .NET Framework 3.5 的舊系統環境下,產出的 Excel 報表在不同版本(例如 Excel 2010/2016 與 Excel 2021/2024)直接列印時,會出現「橫向切到下一頁」或「右側多出一欄空白」的排版不一致問題。
核心原因分析:
- 字型渲染引擎演進(標楷體效應):新舊版 Excel 對「標楷體」的字元寬度計算像素不同(GDI vs DirectWrite)。相同的標楷體文字在新版中渲染出來的總寬度通常會多出幾個像素。如果文字因為多出的像素而折行,雖然目前系統列高預留得夠高、高度沒差,但依然會觸發 Excel 盲測邊界失準,導致橫向被切頁或多印空白欄。
- NPOI 2.1 底層 Bug:原生的
workbook.SetPrintArea存在著名的 XML 節點建構 Bug,在新版 Excel 中會直接導致導出檔案被判定為「檔案已損毀,需要修復」,因此無法使用設定列印區域的方式來限制範圍。 - 多人連線檔案鎖定:如果直接操作、修改並儲存伺服器上的實體模板,在 Web 多人同時連線產出報表時,會引發「檔案被另一個程序佔用」的嚴重衝突。
2. 終極解決方案架構:記憶體隔離克隆法 (Thread-Safe)
不要建立全新的 Workbook 並逐一複製樣式,也絕不污染實體模板。改用以唯讀模式將模板讀入記憶體 ➔ 在記憶體中進行 Sheet 克隆與字串替換 ➔ 清理邊界 ➔ 存檔前刪除樣板 的閉環流程。
核心開發流程與程式碼實作 (C#)
using System;
using System.IO;
using NPOI.SS.UserModel;
public class ExcelReportGenerator
{
public void GenerateReport()
{
string templatePath = @"C:\Templates\ReportTemplate.xlsx";
string outputPath = @"C:\Outputs\FinalReport.xlsx";
// 預期報表的最大總欄數(假設固定為 8 欄,索引 0 ~ 7)
int maxValidColumnIndex = 7;
// 1. 使用 FileShare.Read 唯讀模式開啟實體檔案,保證多人同時連線不鎖檔
using (FileStream fs = new FileStream(templatePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
// 2. 將模板內容完整載入記憶體
byte[] templateBytes;
using (MemoryStream ms = new MemoryStream())
{
byte[] buffer = new byte[1024 * 4];
int read;
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
templateBytes = ms.ToArray();
}
// 3. 在完全隔離的記憶體中建立獨立的 Workbook 物件
using (MemoryStream workbookMs = new MemoryStream(templateBytes))
{
IWorkbook workbook = WorkbookFactory.Create(workbookMs);
// 4. 利用 CloneSheet 完美克隆樣板,完整帶走字型、預設邊界與頁面配置
int templateIndex = 0; // 假設原始樣板在第一個 Sheet
ISheet newSheet = workbook.CloneSheet(templateIndex);
workbook.SetSheetName(workbook.GetSheetIndex(newSheet), "正式報表");
// 5. 執行您原有的 Find and Replace 邏輯
// 註:標楷體即使因為版本差而折行,因高度安全故不會影響縱向分頁
PerformFindAndReplace(newSheet);
// ============================================================
// 防線一:補上底層 PageSetup 縮放,強制鎖定橫向寬度(根治「切到下一頁」)
// ============================================================
newSheet.FitToPage = true;
newSheet.PrintSetup.Scale = 0; // 關鍵:在 NPOI 2.1 中,必須將 Scale 歸零,FitWidth 才會在所有 Excel 版本中生效
newSheet.PrintSetup.FitWidth = 1; // 強制所有欄位橫向壓縮在一頁內
newSheet.PrintSetup.FitHeight = 0; // 高度設為 0,代表縱向不限制,隨資料自然往下延伸
// ============================================================
// 防線二:徹底拔除右側隱形/殘留儲存格(替代 SetPrintArea,根治「多一欄」)
// ============================================================
for (int rowNum = 0; rowNum <= newSheet.LastRowNum; rowNum++)
{
IRow row = newSheet.GetRow(rowNum);
if (row == null) continue;
// 如果 NPOI 判定此行在 XML 中的最後一欄超過了實質資料的上限
if (row.LastCellNum > (maxValidColumnIndex + 1))
{
// 倒序迴圈,將指定欄位之後的所有隱形/空儲存格物件物理性地徹底拔除
for (int c = row.LastCellNum - 1; c > maxValidColumnIndex; c--)
{
ICell cell = row.GetCell(c);
if (cell != null)
{
row.RemoveCell(cell);
}
}
}
}
// ============================================================
// 防線三:安全刪除原始樣板工作表,避開打開空白與重複列印
// ============================================================
// 技巧:先將活動焦點(ActiveSheet)移至新報表,再進行刪除
workbook.SetActiveSheet(workbook.GetSheetIndex(newSheet));
workbook.RemoveSheetAt(templateIndex);
// 6. 將專屬的報表匯出成獨立新檔(或直接 Response 給瀏覽器下載)
using (FileStream outFs = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
{
workbook.Write(outFs);
}
}
}
}
private void PerformFindAndReplace(ISheet sheet)
{
// 您的 Find and Replace 實作代碼
// 提示:只更新 cell.SetCellValue(),絕對不要重新 CreateCell(),以完整保留標楷體格式與邊框
}
}
3. 關鍵執行與相容性檢查清單
- 只換值、不重造:在
Find and Replace時,請直接使用cell.SetCellValue(newValue),不要重新CreateCell,否則會把原模板上精心設計的標楷體字型、置中、邊框等 CellStyle 屬性完全洗掉。 Scale = 0是必備開關:在 NPOI 2.1 底層 XML 結構中,如果沒有明確指定PrintSetup.Scale = 0,部分 Office 版本在讀取FitWidth = 1時會發生衝突進而失效。- 活動焦點移轉:在呼叫
workbook.RemoveSheetAt(0)刪除空白樣板前,必須先呼叫workbook.SetActiveSheet()鎖定新工作表,否則使用者開啟 Excel 時畫面可能因焦點遺失而顯示全白(需手動點選下方分頁才看得到內容)。