chore: 首次簽入 Thinkyu ASP.NET 專案

- 加入 Visual Studio / ASP.NET .gitignore
- 排除建置輸出、IDE 設定、NuGet packages、大型 MSI 安裝檔
This commit is contained in:
2026-09-10 09:42:37 +08:00
commit 577060bc78
2496 changed files with 501389 additions and 0 deletions
@@ -0,0 +1,123 @@
# NPOI 2.1 (.NET 3.5) 跨版本 Excel 列印跑版終極解決方案文件
## 1. 問題背景與根本原因
在使用 **NPOI 2.1** 配合 **.NET Framework 3.5** 的舊系統環境下,產出的 Excel 報表在不同版本(例如 Excel 2010/2016 與 Excel 2021/2024)直接列印時,會出現「**橫向切到下一頁**」或「**右側多出一欄空白**」的排版不一致問題。
### 核心原因分析:
1. **字型渲染引擎演進(標楷體效應)**:新舊版 Excel 對「標楷體」的字元寬度計算像素不同(GDI vs DirectWrite)。相同的標楷體文字在新版中渲染出來的總寬度通常會多出幾個像素。如果文字因為多出的像素而折行,雖然目前系統列高預留得夠高、高度沒差,但依然會觸發 Excel 盲測邊界失準,導致橫向被切頁或多印空白欄。
2. **NPOI 2.1 底層 Bug**:原生的 `workbook.SetPrintArea` 存在著名的 XML 節點建構 Bug,在新版 Excel 中會直接導致導出檔案被判定為「檔案已損毀,需要修復」,因此無法使用設定列印區域的方式來限制範圍。
3. **多人連線檔案鎖定**:如果直接操作、修改並儲存伺服器上的實體模板,在 Web 多人同時連線產出報表時,會引發「檔案被另一個程序佔用」的嚴重衝突。
---
## 2. 終極解決方案架構:記憶體隔離克隆法 (Thread-Safe)
不要建立全新的 Workbook 並逐一複製樣式,也絕不污染實體模板。改用**以唯讀模式將模板讀入記憶體 ➔ 在記憶體中進行 Sheet 克隆與字串替換 ➔ 清理邊界 ➔ 存檔前刪除樣板** 的閉環流程。
### 核心開發流程與程式碼實作 (C#)
```csharp
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. 關鍵執行與相容性檢查清單
1. **只換值、不重造**:在 `Find and Replace` 時,請直接使用 `cell.SetCellValue(newValue)`,不要重新 `CreateCell`,否則會把原模板上精心設計的標楷體字型、置中、邊框等 CellStyle 屬性完全洗掉。
2. **`Scale = 0` 是必備開關**:在 NPOI 2.1 底層 XML 結構中,如果沒有明確指定 `PrintSetup.Scale = 0`,部分 Office 版本在讀取 `FitWidth = 1` 時會發生衝突進而失效。
3. **活動焦點移轉**:在呼叫 `workbook.RemoveSheetAt(0)` 刪除空白樣板前,必須先呼叫 `workbook.SetActiveSheet()` 鎖定新工作表,否則使用者開啟 Excel 時畫面可能因焦點遺失而顯示全白(需手動點選下方分頁才看得到內容)。