使用WPF元件和FreeSpire.PDF建立PDF
當需要在 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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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">
<TextBlock Text="Name:"/> <TextBox Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Margin="{StaticResource Margin.Top}" Text="ID:"/> <TextBox Text="{Binding Path=Id, UpdateSourceTrigger=PropertyChanged}"/>
<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 中:
- 使用 TextBlock 來顯示學生的基本信息(姓名、學號、Email),這三個元素的內容使用Binding 而來,並搭配 Border 做出表格邊線。
- 使用 DataGrid 來顯示學生的各科目的成績,顯示的內容也是透過 Binding 到一個集合來顯示,而外觀部分簡單做了 Style 讓元件看起來略像表格。

使用者輸入區域的部分是一個簡單的表單,如下圖所示。其架構分為:
- 簡單的輸入 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) { 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) });
await Task.Delay(1);
var xpsStream = ConvertToXps(fd);
SaveAsPdf(xpsStream, "output.pdf"); MessageBox.Show("Finish!"); }
private static MemoryStream ConvertToXps(FlowDocument flowDocument) { 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) { var pdfDoc = new PdfDocument(); pdfDoc.LoadFromXPS(xpsStream);
pdfDoc.PageSettings.Size = PdfPageSize.A4;
pdfDoc.SaveToFile(outputPdfPath); pdfDoc.Close();
xpsStream.Dispose(); } }
|
程式執行後,所產生的 PDF 檔內容,如下圖。

總結
在產生 PDF 這一塊,除了手動撰寫程式產生 PDF 排版與資料外,還有一些方法可以實現,例如,可以使用空白的 PDF 作為底稿,然後將需要的資料寫入其中,等等。
而直接使用 XAML 的 View ,再透過 Binding 來放入需要顯示的資料,最後透過 FreeSpire.PDF 套件轉存成 PDF。並且能 Binding 的話,表示不只文字,甚至圖片與集合都可以使用。如果遇到建立的 PDF 有語系的需求,也可以使用 DynamicResource 來切換,使用起來感覺方便不少。
此方法做出的 PDF 目前看起來沒有太大的問題,若是有遇到什麼問題,會再進行文章的更新。
而 UI 的部分,不一定要使用 FlowDocument 也可使用其他 UI 元件,只是再轉 XPS 的時候,則需要用另外的程式來實現。