使用 WebView2 封装一个生成 PDF 的 WPF 控件

最近在迁移项目到 .net6,发现项目中用的 PDF 库不支持 .net6,于是想着换一个库。结果找了一大圈,发现不是版本不支持,就是收费。

嗐!还能咋办,只能自己搞一个 PDF 生成控件咯。

环境准备 WPF + WebView2 + Vue

WebView2

  • WebView2.CoreWebView2.PrintToPdfAsync 可以将 html 文件生成 pdf。
  • CEF 也有类似的 API,Evergreen WebView2 会自动更新,而且不需要将库打包到程序中,所以就用它了。
  • WebView2 需要先安装到本机,下载链接

Vue

  • 直接操作 Dom 不够方便,Vue 用法跟 WPF 的绑定方式又很相似,使用 vue 来定义 pdf 的 Html 的模板,可以让不会 h5 的同事也能轻松写模板文件,所以这里用 Vue 来操作 Dom 和数据绑定。

Prism

  • WPF 项目常用的框架,我这里用来注册预览 PDF 的弹窗,以及给弹窗传参。

以打印一个表格为例

1. 定义要生成 PDF 的表格

// BuyBookView.xaml
<DataGrid
Grid.Row="1"
Margin="24,0"
AutoGenerateColumns="False"
FontSize="16"
IsReadOnly="True"
ItemsSource="{Binding Books}"
TextBlock.TextAlignment="Center"> <DataGrid.Columns>
<DataGridTextColumn
Width="*"
Binding="{Binding Title}"
Header="书名"
HeaderStyle="{StaticResource CenterGridHeaderStyle}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Author}"
Header="作者"
HeaderStyle="{StaticResource CenterGridHeaderStyle}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Price}"
Header="价格"
HeaderStyle="{StaticResource CenterGridHeaderStyle}" />
</DataGrid.Columns>
</DataGrid> // BuyBookViewModel
public BuyBookViewModel(IDialogService dialogService)
{
Title = "鸭霸的购书目录";
Books = new List<Book>
{
new()
{
Title = "JavaScript权威指南 原书第7版",
Author = "巨佬1",
Price = 90.3
},
new()
{
Title = "深入浅出node.js",
Author = "巨佬2",
Price = 57.8
},
new()
{
Title = "编码:隐匿在计算机软硬件背后的语言",
Author = "巨佬3",
Price = 89.00
}
};
}

2. 定义预览 PDF 的弹窗

  • 在 xaml 中引入 WebView2
// PrintPdfView.xml
...
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
... <Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> <Grid>
<wpf:WebView2 x:Name="webView2" />
</Grid>
<Grid Grid.Row="1">
<Button
x:Name="save"
HorizontalAlignment="Right"
Content="保存" />
</Grid>
</Grid>
  • 在 viewmodel 中定义弹窗接收的参数以及弹窗的属性
// PrintPdfViewModel.cs
public class PrintPdfViewModel : BindableBase, IDialogAware
{
private string _template;
/// <summary>
/// PDF 的 html 模板
/// </summary>
public string Template
{
get => _template;
set => SetProperty(ref _template, value);
} private ExpandoObject _data;
/// <summary>
/// 传递给 pdf 的数据
/// </summary>
public ExpandoObject Data
{
get => _data;
set => SetProperty(ref _data, value);
} public void OnDialogOpened(IDialogParameters parameters)
{
// 弹窗接收 template 和 data 两个参数
parameters.TryGetValue("template", out _template);
parameters.TryGetValue("data", out _data);
} public string Title => "预览 PDF";
}

3. 定义 WebView2 生成 PDF 的逻辑和 pdf 的模板文件

  • 使用 vue 来定义 pdf 模板的逻辑,和调用 WebView2.CoreWebView2.PrintToPdfAsync 来生成 PDF。
  • 因为客户端经常运行在内网或无网环境,所以这里就不用 cdn 引入 vuejs,而是直接将 vuejs 嵌入到客户端的资源文件中。
  • 调用 WebView2.CoreWebView2.PostWebMessageAsJson 从 WPF 向 WebView2 发送数据。
// PrintPdfViewModel.xaml.cs
/// <summary>
/// 配置 WebView2,加载 vuejs,加载 pdf 模板,传递数据到 html 中
/// </summary>
/// <returns></returns>
private async Task Load()
{
await webView2.EnsureCoreWebView2Async();
webView2.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; // 禁止右键菜单 var assembly = Assembly.GetExecutingAssembly();
var resourceName = "PrintPdf.Views.vue.global.js"; using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream != null)
{
using var reader = new StreamReader(stream);
var vue = await reader.ReadToEndAsync();
await webView2.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(vue); // 加载 vuejs
} var vm = (PrintPdfViewModel)DataContext; webView2.CoreWebView2.NavigateToString(vm.Template); // 加载 pdf 模板 webView2.CoreWebView2.NavigationCompleted += (sender, args) =>
{
var json = JsonSerializer.Serialize(vm.Data);
webView2.CoreWebView2.PostWebMessageAsJson(json); // 将数据传递到 html 中
};
}
  • 点击保存时,选择路径并生成 PDF 文件。
// PrintPdfViewModel.xaml.cs
save.Click += async (sender, args) =>
{
var saveFileDialog = new SaveFileDialog
{
Filter = "txt files (*.pdf)|*.pdf",
RestoreDirectory = true,
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
FileName = $"test.pdf"
};
var result = saveFileDialog.ShowDialog(); if (result != true)
return; var printSetting = webView2.CoreWebView2.Environment.CreatePrintSettings();
printSetting.ShouldPrintBackgrounds = true; var saveResult = await webView2.CoreWebView2.PrintToPdfAsync($"{saveFileDialog.FileName}", printSetting);
};
  • 定义 pdf 的打印模板,并且使用 Vue 来实现绑定功能,调用 webview.addEventListener 来监听 WPF 传递给 WebView2 的数据。
<html lang="en">
<head>
...
</head> <body>
<div id="app">
<div id="header">
<h3>
{{title}}
</h3>
</div>
<div id="content">
<table>
<thead>
<tr>
<th>序号</th>
<th>书名</th>
<th>作者</th>
<th>价格</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, i) in books">
<th>{{i+1}}</th>
<td>{{item.Title}}</td>
<td>{{item.Author}}</td>
<td>{{item.Price}}</td>
</tr>
</tbody>
</table> </div>
</div>
</body>
<script>
// 调用 webview.addEventListener 来监听 WPF 传递给 WebView2 的数据。
window.chrome.webview.addEventListener('message', event => generate(event.data));
// 完成数据绑定
function generate(data) {
const app = Vue.createApp({
data() {
return {title, books} = data;
},
});
app.mount('#app');
}
</script> </html>
  • 在 WPF 客户端点击生成 PDF 时,打开 PDF 预览窗口,并且传递模板和数据给 WebView2
// BuyBookView.xaml
<Button Command="{Binding ShowPrintViewCommand}" Content="预览 PDF1 " /> // BuyBookViewModel
ShowPrintViewCommand = new DelegateCommand(() =>
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"PrintPdf.ViewModels.test_print.html"; using var stream = assembly.GetManifestResourceStream(resourceName); // 加载模板
if (stream == null) return;
using var reader = new StreamReader(stream);
var t = reader.ReadToEnd();
dynamic d = new ExpandoObject(); // 转换数据
d.title = Title;
d.books = Books; var p = new DialogParameters
{
{"template", t},
{"data", d}
};
dialogService.ShowDialog(nameof(PrintPdfView), p, null);
});

4. 效果

5. 优化一下

现在功能已经差不多了,但是 html 模板需要写的 js 太多,而且这是一个 WPF 控件,所以应该封装一下,最好用起来跟 wpf 一样才更好。

既然都用 vue 了,那就用 vue 封装一下组件。

  • vue 封装一下表格控件,并且暴露出属性 itemSource 和 columns
// controls.js
const DataGrid = {
props: ["itemsSource", "columns"],
template: `
<table style="width: 100%; border-collapse: collapse; border: 1px solid black; ">
<thead>
<tr>
<th v-for="column in columns" style="border: 1px solid black; background-color: lightblue; height: 40px;">
{{column.Header}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in itemsSource">
<td v-for="column in columns" style="text-align: center; vertical-align: middle; border: 1px solid black; height: 32px;">
{{item[column.Binding]}}
</td>
</tr>
</tbody>
</table>
`
}
const DocumentHeader = {
props: ["title"],
template: `
<div style="width: 70%; height: 100px; margin: 0 auto; display: flex; align-items: center; justify-content: center;">
<h2>{{title}}</h2>
</div>
`
};
  • 将 controls.js 注入到 WebView2 中
var assembly = Assembly.GetExecutingAssembly();
var controlsFile = "PrintPdf.Views.controls.js"; using var controlsStream = assembly.GetManifestResourceStream(controlsFile); using var controlsReader = new StreamReader(controlsStream);
var controls = await controlsReader.ReadToEndAsync();
await webView2.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(controls);
  • 现在 html 模板中的 data-grid 组件就跟 WPF 的 DataGrid 控件很相似了
<html lang="en">

<head>
...
</head> <body>
<div id="app">
<document-header :title="title"></document-header>
<data-grid :items-source="books" :columns="columns"></data-grid>
</div>
</body> <script>
window.chrome.webview.addEventListener('message', event => generate(event.data)); function generate(data) {
Vue.createApp({
data() {
return {
title,columns,books
} = data; },
components: {
DataGrid,
DocumentHeader
}
}).mount('#app');
} </script> </html>

最后

觉得对你有帮助点个推荐或者留言交流一下呗!

源码 https://github.com/yijidao/blog/tree/master/WPF/PrintPdf

使用 WebView2 封装一个生成 PDF 的 WPF 控件的更多相关文章

  1. 示例:WPF中Slider控件封装的缓冲播放进度条控件

    原文:示例:WPF中Slider控件封装的缓冲播放进度条控件 一.目的:模仿播放器播放进度条,支持缓冲任务功能 二.进度: 实现类似播放器中带缓存的播放样式(播放区域.缓冲区域.全部区域等样式) 实现 ...

  2. 浅尝辄止——使用ActiveX装载WPF控件

    1 引言 使用VC编写的容器类编辑器,很多都可以挂接ActiveX控件,因为基于COM的ActiveX控件不仅封装性不错,还可以显示一些不错的界面图元. 但是随着技术不断的进步,已被抛弃的Active ...

  3. 反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑) C#中缓存的使用 C#操作redis WPF 控件库——可拖动选项卡的TabControl 【Bootstrap系列】详解Bootstrap-table AutoFac event 和delegate的分别 常见的异步方式async 和 await C# Task用法 c#源码的执行过程

    反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑)   背景介绍: 为了平衡社区成员的贡献和索取,一起帮引入了帮帮币.当用户积分(帮帮点)达到一定数额之后,就会“掉落”一定数量的“帮帮 ...

  4. 《Dotnet9》系列-开源C# WPF控件库3《HandyControl》强力推荐

    大家好,我是Dotnet9小编,一个从事dotnet开发8年+的程序员.我最近开始写dotnet分享文章,希望能让更多人看到dotnet的发展,了解更多dotnet技术,帮助dotnet程序员应用do ...

  5. WPF 控件库——仿制Chrome的ColorPicker

    WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...

  6. WPF 控件库——可拖动选项卡的TabControl

    WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...

  7. wpf控件设计时支持(3)

    原文:wpf控件设计时支持(3) wpf设计时调试 编辑模型 装饰器 1.wpf设计时调试 为了更好的了解wpf设计时框架,那么调试则非常重要,通过以下配置可以调试控件的设计时代码 (1)将启动项目配 ...

  8. 国内开源C# WPF控件库Panuon.UI.Silver推荐

    国内优秀的WPF开源控件库,Panuon.UI的优化版本.一个漂亮的.使用样式与附加属性的WPF UI控件库,值得向大家推荐使用与学习. 今天站长(Dotnet9,站长网址:https://dotne ...

  9. 国内开源C# WPF控件库Panuon.UI.Silver强力推荐

    国内优秀的WPF开源控件库,Panuon.UI的优化版本.一个漂亮的.使用样式与附加属性的WPF UI控件库,值得向大家推荐使用与学习. 今天站长(Dotnet9,站长网址:https://dotne ...

随机推荐

  1. Python实训day07pm【Selenium操作网页、爬取数据-下载歌曲】

    练习1-爬取歌曲列表 任务:通过两个案例,练习使用Selenium操作网页.爬取数据.使用无头模式,爬取网易云的内容. ''' 任务:通过两个案例,练习使用Selenium操作网页.爬取数据. 使用无 ...

  2. 1.配置桥接,并抓包验证 2.实现免密登录 3.修改登录端口: 22-》2222 4.不允许root用户远程登录 5.创建用户sshuser1,并设置密码,且只允许sshuser1远程ssh登录

    1.配置桥接:  抓包时如果有ens160的ICMP,说明我们的桥接搭建成功通过桥接访问到了ens160(这里忘加图片了) (1)创建一个桥接设备和会话 (2)添加设备和会话到桥接设备上 (3)启动从 ...

  3. 《剑指offer》面试题64. 求1+2+…+n

    问题描述 求 1+2+...+n ,要求不能使用乘除法.for.while.if.else.switch.case等关键字及条件判断语句(A?B:C). 示例 1: 输入: n = 3 输出: 6 示 ...

  4. NPOI处理Excel

    using NPOI; using NPOI.XSSF.UserModel; using NPOI.SS.UserModel; using NPOI.HSSF.UserModel; NPOI.SS.U ...

  5. 【算法】nSum问题

    LeetCode中出现了2sum, 3sum, 4sum的问题,文章给出了一种通用的解法,想法是将n_sum问题转换为(n-1)_sum问题,具体步骤如下: 定义函数sum(n, target),表示 ...

  6. 【刷题-LeetCode】122 Best Time to Buy and Sell Stock II

    Best Time to Buy and Sell Stock II Say you have an array for which the ith element is the price of a ...

  7. 【记录一个问题】golang的xorm组件更新数据库未生效

    代码中使用了类似的方式来更新数据库: func (writer *dbWriter) updateVersion(ctx context.Context, IP string, version str ...

  8. 【记录一个问题】用ndk的gcc命令行无法编译C++11的lambda等语法的代码

    /Users/ahfu/code/android/android-ndk-r14b/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_6 ...

  9. seq2seq+torch7聊天机器人bug处理

    [C]: in function 'error' ...root1/torch/install/share/lua/5.2/rnn/recursiveUtils.lua:44: in function ...

  10. node.js在Linux下执行shell命令、.sh脚本

    首先,引入子进程模块 var process = require('child_process'); 执行shell命令 调用该模块暴露出来的方法exec process.exec('shutdown ...