抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

當需要在 C# WPF 中產生 PDF 時,通常會使用各種功能強大的套件來實現。那代表著需要學習並掌握套件內的類別與方法,才能實現需要的排版。並且過程中還要確保將顯示的資料正確的放入到 PDF 中。

總想著,為什麼製作 PDF 不能像編寫 WPF XAML 一樣簡單呢? 這樣就不用學習其他套件了,而且還能透過 Data Binding 直接將資料綁訂到 UI 上。這樣一來,整個過程應該會很方便吧?

於是經過 Study 後,可以使用 WPF 元件。然後,將其轉成 XPS 格式後,再使用 FreeSpire.PDF 套件中的方法,將 XPS 存成 PDF。這樣就能簡單的通過撰寫 XAML 來產生 PDF檔。

本次將放上簡單的範例來記錄這項功能的使用。

本次的開發環境,如下

  • IDE : Visual Studio 2022
  • .NET版本 : . NET 6.0
  • Nuget套件: CommunityToolkit.Mvvm 8.2.2、FreeSpire.PDF 8.6.0

本文

首先來看 View 的部分,完整的 XAML 如下,

分為兩大部分,分別用途為:

  • 用於建立 PDF 檔案:假設要產生一個簡單的學生成績單,整體 UI 隱藏在下層。
  • 使用者輸入區域:用於輸入簡單的資料,透過 Binding 讓 PDF 能有對應的內容。

XAML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
<Window.Resources>

<Thickness x:Key="Margin.Top">0,10,0,0</Thickness>

<!--TitleTextBlockStyle-->
<Style x:Key="TitleTextBlockStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="0,20,0,5"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>

<!--TableTitleTextBlockStyle-->
<Style x:Key="TableTitleTextBlockStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="0,0,5,0"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="12"/>
</Style>

<!--TableContentTextBlockSytle-->
<Style x:Key="TableContentTextBlockSytle" TargetType="TextBlock">
<Setter Property="Margin" Value="5,0,0,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
</Style>

<!--DataGridColumnHeaderStyle-->
<Style x:Key="DataGridColumnHeaderStyle" TargetType="DataGridColumnHeader">
<Setter Property="Height" Value="30"/>
<Setter Property="FontWeight" Value="Bold"/>

<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridColumnHeader">
<Border Width="Auto"
BorderThickness="0,1"
BorderBrush="Black"
Background="White">
<Grid>
<ContentPresenter Margin="5,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"/>
<Rectangle Width="1"
Fill="Black"
HorizontalAlignment="Right"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

</Window.Resources>

<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>

<Grid>

<!--用於建立PDF檔案-->
<FlowDocumentScrollViewer VerticalScrollBarVisibility="Disabled"
HorizontalScrollBarVisibility="Disabled"
Visibility="Visible">

<FlowDocument x:Name="flowDocument"
Background="White"
PageWidth="595"
ColumnWidth="595"
PageHeight="842"
PagePadding="40"
IsOptimalParagraphEnabled="False"
IsHyphenationEnabled="False">

<Paragraph TextAlignment="Center"
FontSize="22pt">
<Run Text="Student Report Card"/>
</Paragraph>

<BlockUIContainer>

<StackPanel>

<!--Student Information-->
<TextBlock Style="{StaticResource TitleTextBlockStyle}"
Text="Student Information"/>

<Grid Margin="50,0"
Height="90">

<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>

<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>

<!--Name-->
<Border Grid.Row="0"
Grid.Column="0"
BorderBrush="Black"
BorderThickness="1,1,0,0">
<TextBlock Style="{StaticResource TableTitleTextBlockStyle}"
Text="Name"/>
</Border>

<Border Grid.Row="0"
Grid.Column="1"
BorderBrush="Black"
BorderThickness="1,1,1,0">
<TextBlock Style="{StaticResource TableContentTextBlockSytle}"
Text="{Binding Path=Name}"/>
</Border>

<!--ID-->
<Border Grid.Row="1"
Grid.Column="0"
BorderBrush="Black"
BorderThickness="1,1,0,0">
<TextBlock Style="{StaticResource TableTitleTextBlockStyle}"
Text="ID"/>
</Border>

<Border Grid.Row="1"
Grid.Column="1"
BorderBrush="Black"
BorderThickness="1,1,1,0">
<TextBlock Style="{StaticResource TableContentTextBlockSytle}"
Text="{Binding Path=Id}"/>
</Border>

<!--Email-->
<Border Grid.Row="2"
Grid.Column="0"
BorderBrush="Black"
BorderThickness="1,1,0,1">
<TextBlock Style="{StaticResource TableTitleTextBlockStyle}"
Text="Email"/>
</Border>

<Border Grid.Row="2"
Grid.Column="1"
BorderBrush="Black"
BorderThickness="1">
<TextBlock Style="{StaticResource TableContentTextBlockSytle}"
Text="{Binding Path=Email}"/>
</Border>

</Grid>

<!--Grades-->
<TextBlock Style="{StaticResource TitleTextBlockStyle}"
Text="Grades"/>

<DataGrid Margin="50,0"
FontSize="12"
RowHeaderWidth="0"
AutoGenerateColumns="False"
CanUserAddRows="False"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"
ItemsSource="{Binding Path=GradeCollection}">

<DataGrid.Columns>
<DataGridTextColumn Header="Subject"
Width="140"
Binding="{Binding Path=Subject}"/>
<DataGridTextColumn Header="Grade"
Width="*"
Binding="{Binding Path=Grade}"/>
</DataGrid.Columns>
</DataGrid>

</StackPanel>

</BlockUIContainer>

</FlowDocument>

</FlowDocumentScrollViewer>

<!--使用者輸入區域-->
<Grid Background="White"
Visibility="Visible">

<StackPanel Margin="30">

<!--Name-->
<TextBlock Text="Name:"/>
<TextBox Text="{Binding Path=Name,
UpdateSourceTrigger=PropertyChanged}"/>

<!--Id-->
<TextBlock Margin="{StaticResource Margin.Top}"
Text="ID:"/>
<TextBox Text="{Binding Path=Id,
UpdateSourceTrigger=PropertyChanged}"/>

<!--Email-->
<TextBlock Margin="{StaticResource Margin.Top}"
Text="Email:"/>
<TextBox Text="{Binding Path=Email,
UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="{StaticResource Margin.Top}"
HorizontalAlignment="Left"
Width="120" Height="30"
Content="Create"
Command="{Binding Path=CreatePdfCommand}"
CommandParameter="{Binding ElementName=flowDocument}"/>

</StackPanel>

</Grid>

</Grid>

在建立 PDF 的部分,整體效果如下圖所示。整體的架構大致分為:

  • 最外層使用了 FlowDocument 來定義整個頁面。
  • 使用 Paragraph 元素來顯示 PDF 文檔的標題「Student Report Card」,也可以用其他 UI 元件。
  • 使用 BlockUIContainer 包含了主要的 UI 元素,來 Binding 與 顯示內容。
  • 主要顯示的內容,放在 StackPanel 中:
    1. 使用 TextBlock 來顯示學生的基本信息(姓名、學號、Email),這三個元素的內容使用Binding 而來,並搭配 Border 做出表格邊線。
    2. 使用 DataGrid 來顯示學生的各科目的成績,顯示的內容也是透過 Binding 到一個集合來顯示,而外觀部分簡單做了 Style 讓元件看起來略像表格。

在 WPF 程式中,使用 XAML 所做出的學生成績單的畫面

使用者輸入區域的部分是一個簡單的表單,如下圖所示。其架構分為:

  • 簡單的輸入 UI,可以輸入姓名、學號和 Email。
  • 產生按鈕,按下按鈕後,將執行對應的程式來建立 PDF 檔案。成績的部分則會在建立過程中隨機產生並包含在PDF中。

接下來是 ViewModel 的部分。由於成績的資料會是一個集合,因此需要建立一個類別來放置成績,程式碼如下。

GradeObservableModel

1
2
3
4
5
6
7
8
public partial class GradeObservableModel : ObservableObject
{
[ObservableProperty]
private string? _subject;

[ObservableProperty]
private int _grade;
}

最後是 MainWindow 所用的 ViewModel,程式碼如下。

在 Class 方面,ViewModel 的機制藉由繼承了 CommunityToolkit.MVVM 套件中的 ObservableObject 類別來達成。

在 Property 方面,有 Name、Id、Email 以及 GradeCollection 。

在 Command 方面,執行流程為,

參數會帶入 FlowDocument。
清空 GradeCollection ,產生新的一組資料。
使用 ConvertToXps 方法將 FlowDocument 轉換成 XPS 格式。
將 XPS 資料帶入 SaveAsPdf 方法,該方法使用 FreeSpire.PDF 套件中的方法讀取 XPS 資料並儲存為 PDF 檔案。

MainWindowViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public partial class MainWindowViewModel : ObservableObject
{
private Random random;

[ObservableProperty]
private string? _name;

[ObservableProperty]
private string? _id;

[ObservableProperty]
private string? _email;

public ObservableCollection<GradeObservableModel> GradeCollection
{ get; set; }

public MainWindowViewModel()
{
random = new();

_name = "Peter";
_id = "123456";
_email = "peter@email.com";

GradeCollection = new();
}

[RelayCommand]
private async Task CreatePdf(FlowDocument fd)
{
// Clear GradeCollection and add random data
GradeCollection.Clear();
GradeCollection.Add(new() { Subject = "Math", Grade = random.Next(60, 101) });
GradeCollection.Add(new() { Subject = "English", Grade = random.Next(60, 101) });
GradeCollection.Add(new() { Subject = "Science", Grade = random.Next(60, 101) });

// Wait for UI refresh
await Task.Delay(1);

// Convert the FlowDocument to XPS
var xpsStream = ConvertToXps(fd);

// Save PDF to the specified path
SaveAsPdf(xpsStream, "output.pdf");
MessageBox.Show("Finish!");
}

private static MemoryStream ConvertToXps(FlowDocument flowDocument)
{
// Convert FlowDocument to XPS stream
var xpsStream = new MemoryStream();
using (Package package = Package.Open(xpsStream, FileMode.Create))
{
var xpsDoc = new XpsDocument(package);
XpsDocumentWriter xpsWriter = XpsDocument.CreateXpsDocumentWriter(xpsDoc);
DocumentPaginator paginator = ((IDocumentPaginatorSource)flowDocument).DocumentPaginator;
xpsWriter.Write(paginator);
xpsDoc.Close();
}

return xpsStream;
}

private static void SaveAsPdf(MemoryStream xpsStream, string outputPdfPath)
{
// Load XPS stream into PdfDocument
var pdfDoc = new PdfDocument();
pdfDoc.LoadFromXPS(xpsStream);

// Set PDF page size to A4
pdfDoc.PageSettings.Size = PdfPageSize.A4;

// Save PdfDocument to PDF file
pdfDoc.SaveToFile(outputPdfPath);
pdfDoc.Close();

// Dispose the XPS stream
xpsStream.Dispose();
}
}

程式執行後,所產生的 PDF 檔內容,如下圖。
程式執行後,所產生的 PDF 檔內容

總結

在產生 PDF 這一塊,除了手動撰寫程式產生 PDF 排版與資料外,還有一些方法可以實現,例如,可以使用空白的 PDF 作為底稿,然後將需要的資料寫入其中,等等。

而直接使用 XAML 的 View ,再透過 Binding 來放入需要顯示的資料,最後透過 FreeSpire.PDF 套件轉存成 PDF。並且能 Binding 的話,表示不只文字,甚至圖片與集合都可以使用。如果遇到建立的 PDF 有語系的需求,也可以使用 DynamicResource 來切換,使用起來感覺方便不少。

此方法做出的 PDF 目前看起來沒有太大的問題,若是有遇到什麼問題,會再進行文章的更新。

而 UI 的部分,不一定要使用 FlowDocument 也可使用其他 UI 元件,只是再轉 XPS 的時候,則需要用另外的程式來實現。

評論




本站使用 Volantis 作為主題,總訪問量為