使用 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. 使用VUE组件创建SpreadJS自定义单元格(二)

    在上篇中,我们介绍了如何通过设置runtimeCompiler为true,在Vue中实现了动态创建电子表格组件.想了解具体内容可看点击查看使用VUE组件创建SpreadJS自定义单元格(一). 但是在 ...

  2. Android开发 海康威视 多路视频播放(同时播放视频)

    原文地址:Android开发 海康视频 多路视频播放 | Stars-One的杂货小窝 最近公司有个项目需要对接到海康监控摄像头来实现对应的实时播放和回放,但这两个不是我们今天要讨论的重点,APP首页 ...

  3. 达索CATIA许可证(License)管理使用和优化

    现下主流的V6版本CATIA,是由达索公司提供授权的浮动型License,其客户端通过企业内网从许可证服务器获得许可证,最少要有一个服务器端DS License Server提供一定数量的Licens ...

  4. vue.config.js报错cannot set property "preserveWhitespace" of undefined

    vue.config.js报错cannot set property "preserveWhitespace" of undefined 最近在项目中配置webpack,由于vue ...

  5. C# app.config 保存和读取例子

    保存: Configuration config = ConfigurationManager.OpenExeConfiguration(Application.ExecutablePath); if ...

  6. [开发笔记usbTOcan]PyUSB访问设备

    前面的几个章节的介绍,基本把usbTOcan的底层代码设计好,现在需要介绍PC端的PyUSB进行简单的测试. 在文章开始之前,需要简单的介绍一下整个系统. 0 | 部署 这里使用了两块TM4C123G ...

  7. Ajax的IE缓存问题

    Ajax之IE缓存问题 <!-- IE浏览器会对ajax的结果进行一个缓存,这样就会导致一个缓存问题 浏览器会读取缓存 而不会去使用一个新的数据 这样对一个时效性比较强的场景 ajax的缓存会影 ...

  8. elasticsearch源码分析及插件开发

    ElasticSearch是一个基于Lucene的搜索服务器.它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口.Elasticsearch是用Java开发的,并作为Apach ...

  9. 004 Linux 揭开神器 vim 面纱

    01 开篇初识 vim vim 功能吊炸天,但我们掌握一些常用的命令即可应对日常的使用了,不记流水账! Linux 中最常用的编辑器是什么? vim ! vi 跟 vim 啥区别? vim 就是 vi ...

  10. Gc如何判断对象可以被回收?

    Gc如何判断对象可以被回收? 1 引用计数器 引用计数法的算法思路:给对象增加一个引用计数器,每当对象增加一个引用计数器+1,失去一个引用-1,所以当计数器是0的时候对象就没有引用了,就会被认为可回收 ...