元件與版面
QuestPDF 的版面系統以 容器 和 元件 為核心。容器負責排列方向和空間分配,元件負責呈現內容。
Component 抽象繼承關係
建立可重用的報表元件時,QuestPDF 提供 3 個關鍵 interface:
IContainer— slot 型別,任何能被「寫入內容」的位置都是它(page.Content()回傳的就是IContainer)。IComponent— 靜態 可重用元件;覆寫Compose(IContainer)把內容畫入給定 slot。IDynamicComponent<TState>— 動態、可分頁元件;覆寫Compose(DynamicContext)並保有跨頁狀態。
下圖是三者與應用層元件的關係:
使用時機對照:
| 抽象 | 何時選用 | 典型範例 |
|---|---|---|
IContainer | 你是「被寫入方」(slot);幾乎所有 Fluent API 參數 | page.Content(), col.Item(), row.RelativeItem() 的回傳值 |
IComponent | 要把一段版面邏輯封裝成可重用單元、且不需要感知分頁狀態 | 報表頁首、公司 logo+標題區塊、簽名欄 |
IDynamicComponent<TState> | 內容需要跨頁保持狀態,例如「第 N 頁接續上一頁的資料列」 | 長資料表的續行、跨頁註腳、已用空間計算 |
使用 IComponent 的呼叫端:
// 呼叫端
page.Header().Component(new TableHeaderComponent {
ReportTitle = "設備參數報表",
GeneratedAt = DateTime.Now
});
// 元件端
public class TableHeaderComponent : IComponent {
public string ReportTitle { get; init; } = "";
public DateTime GeneratedAt { get; init; }
public void Compose(IContainer container) {
container.Column(col => {
col.Item().Text(ReportTitle).FontSize(20).Bold();
col.Item().Text($"產出:{GeneratedAt:yyyy/MM/dd HH:mm}");
});
}
}
讀圖重點:
IComponent與IContainer是兩個方向:前者是「要畫的東西」,後者是「要畫進去的位置」;Compose(IContainer)就是把兩者接起來。IDynamicComponent<TState>的TState不是任意型別:常見用int(當前頁索引)或自訂 struct;QuestPDF 在每次 Compose 後把回傳的DynamicComponentComposeResult.State存下來,下一頁 Compose 時回灌進State屬性。- 不要在
IComponent裡存可變狀態:會破壞 QuestPDF 對「每次 Generate 都從零開始」的假設,造成跨份報表互相污染。有狀態需求一律用IDynamicComponent<TState>。
版面容器
Column — 垂直排列
page.Content().Column(col =>
{
col.Spacing(8); // 每個項目間距 8pt
col.Item().Text("參數 1:溫度");
col.Item().Text("參數 2:壓力");
col.Item().Text("參數 3:轉速");
});
Row — 水平排列
page.Content().Row(row =>
{
// 固定寬度
row.ConstantItem(100).Text("設備名稱:");
// 佔滿剩餘空間
row.RelativeItem().Text("PLC-001");
// 比例分配
row.RelativeItem(2).Text("佔 2/3 寬度");
row.RelativeItem(1).Text("佔 1/3 寬度");
});
巢狀組合
page.Content().Column(col =>
{
col.Item().Row(row =>
{
row.RelativeItem().Column(innerCol =>
{
innerCol.Item().Text("設備:PLC-001");
innerCol.Item().Text("型號:Mitsubishi FX5U");
});
row.ConstantItem(80).Image("device-photo.jpg");
});
});
Table — 表格
基本表格
page.Content().Table(table =>
{
// 定義欄寬
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(3); // 參數名稱(佔 3 份)
columns.RelativeColumn(2); // 數值(佔 2 份)
columns.RelativeColumn(1); // 單位(佔 1 份)
columns.ConstantColumn(80); // 狀態(固定 80pt)
});
// 表頭
table.Header(header =>
{
header.Cell().Background(Colors.Grey.Darken2)
.Padding(5).Text("參數名稱").FontColor(Colors.White).Bold();
header.Cell().Background(Colors.Grey.Darken2)
.Padding(5).Text("數值").FontColor(Colors.White).Bold();
header.Cell().Background(Colors.Grey.Darken2)
.Padding(5).Text("單位").FontColor(Colors.White).Bold();
header.Cell().Background(Colors.Grey.Darken2)
.Padding(5).Text("狀態").FontColor(Colors.White).Bold();
});
// 資料列
foreach (var param in parameters)
{
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).Text(param.Name);
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).AlignRight().Text(param.Value.ToString("F2"));
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).Text(param.Unit);
table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2)
.Padding(5).Text(param.IsNormal ? "正常" : "異常")
.FontColor(param.IsNormal ? Colors.Green.Darken1 : Colors.Red.Darken1);
}
});
合併儲存格
table.Cell().RowSpan(3).Text("區域 A"); // 垂直合併 3 列
table.Cell().ColumnSpan(2).Text("合併欄位"); // 水平合併 2 欄
Text — 文字
基本文字
container.Text("純文字");
container.Text("粗體").Bold();
container.Text("斜體").Italic();
container.Text("大字").FontSize(20);
container.Text("彩色").FontColor(Colors.Red.Medium);
container.Text("底線").Underline();
混合樣式(Rich Text)
container.Text(text =>
{
text.Span("設備 ");
text.Span("PLC-001").Bold().FontColor(Colors.Blue.Medium);
text.Span(" 溫度超標:");
text.Span("92.5°C").Bold().FontColor(Colors.Red.Medium);
text.Span("(上限 80°C)");
});
DefaultTextStyle
在頁面或容器層級設定預設文字樣式:
page.DefaultTextStyle(style =>
style.FontFamily("Microsoft JhengHei")
.FontSize(10)
.FontColor(Colors.Grey.Darken3));
Image — 圖片
// 從檔案
container.Image("company-logo.png");
// 從 byte[]
container.Image(imageBytes);
// 從 Stream
container.Image(stream);
// 控制大小
container.Width(200).Image("device-photo.jpg");
// 填滿可用空間
container.Image("photo.jpg").FitArea();
Page 設定
頁面大小與邊距
page.Size(PageSizes.A4); // A4 直式
page.Size(PageSizes.A4.Landscape()); // A4 橫式
page.Size(new PageSize(100, 50, Unit.Millimetre)); // 自訂(標籤尺寸)
page.MarginTop(2, Unit.Centimetre);
page.MarginBottom(2, Unit.Centimetre);
page.MarginHorizontal(1.5f, Unit.Centimetre);
Header / Footer
page.Header().Row(row =>
{
row.RelativeItem().Column(col =>
{
col.Item().Text("GatherTech 設備參數報表")
.FontSize(14).Bold();
col.Item().Text($"產出日期:{DateTime.Now:yyyy/MM/dd HH:mm}")
.FontSize(8).FontColor(Colors.Grey.Medium);
});
row.ConstantItem(60)
.AlignRight()
.Image("company-logo.png");
});
page.Footer().AlignCenter().Text(text =>
{
text.DefaultTextStyle(s => s.FontSize(8).FontColor(Colors.Grey.Medium));
text.Span("第 ");
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
text.Span(" 頁");
});
進階元件
ShowOnce — 只在第一頁顯示
page.Content().Column(col =>
{
// 封面資訊只在第一頁
col.Item().ShowOnce().Column(inner =>
{
inner.Item().Text("設備驗證報告").FontSize(24).Bold();
inner.Item().Text($"設備:{deviceName}");
inner.Item().Text($"日期:{DateTime.Now:yyyy/MM/dd}");
inner.Item().PaddingBottom(20);
});
// 資料表格會自動分頁
col.Item().Table(table => { /* ... */ });
});
Decoration — 重複的頁首區域
page.Content().Decoration(decoration =>
{
// Before:每頁頂部都會出現
decoration.Before().BorderBottom(1).BorderColor(Colors.Grey.Medium)
.PaddingBottom(5).Text("設備參數清單").Bold();
// Content:會自動分頁
decoration.Content().Table(table => { /* ... */ });
});
PageBreak — 手動分頁
col.Item().PageBreak();
背景色與框線
container
.Background(Colors.Grey.Lighten4)
.Border(1)
.BorderColor(Colors.Grey.Medium)
.Padding(10)
.Text("有框線有背景的區塊");
水印
page.Foreground().Text("機密文件")
.FontSize(60)
.FontColor(Colors.Red.Lighten3)
.Bold();
QuestPDF Previewer
開發時可用 QuestPDF Previewer 即時預覽,不需要每次產出 PDF 檔案:
dotnet add package QuestPDF.Previewer
// 開發環境使用 Previewer
document.ShowInPreviewer(); // 會開啟預覽視窗
Previewer 支援即時更新——修改程式碼、重新執行,預覽視窗自動刷新。
下一步:報表 Pattern — 完整的報表模板與批次產出