工作中需要整理一些pdf格式文件,程序员的存在就是为了让大家可以“懒更高效地工作”,而ai的出现就可以让程序更“懒高效地工作”,于是求助于很长(我指上下文)的gemini,它帮助了我快速搭建项目,但也给我留下了坑(见本文“后记”部分),于是我把这个开发过程记录了下来。
技术选型
- ui框架: wpf (.net 6/7/8 或 .net framework 4.7.2+) - 用于构建现代化的windows桌面应用。
- pdf处理: itext (替代了旧版的 itextsharp 及 itext7) - 一个强大且流行的开源pdf处理库。
- excel导出: npoi - 一个开源的.net库,可以读写office文档,无需安装microsoft office。
- 设计模式: mvvm - 使ui和业务逻辑分离,提高代码的可测试性和复用性。
第一步:创建项目并安装依赖库
打开 visual studio,创建一个新的 wpf 应用程序 项目(本文为.net 8.0项目)。
通过 nuget 包管理器安装以下必要的库。在“解决方案资源管理器”中右键点击你的项目,选择“管理nuget程序包”,然后搜索并安装:
itext
npoi
microsoft.windowsapicodepack-shell
(为了一个更好看的文件夹选择对话框)
第二步:定义数据模型 (model)
这是我们用来存储每个pdf文件信息的类。
pdffileinfo.cs
namespace pdffilescanner { public class pdffileinfo { public string filename { get; set; } = string.empty; public int pagecount { get; set; } public string filesize { get; set; } = string.empty; } }
第三步:创建视图模型 (viewmodel)
viewmodel 是连接视图和模型的桥梁,包含了所有的业务逻辑和ui状态,在这里,我按照ai的提示创建了mainviewmodel
类。
mainviewmodel.cs
using itext.kernel.pdf; using npoi.ss.usermodel; using npoi.xssf.usermodel; using system.collections.objectmodel; using system.componentmodel; using system.io; using system.threading.tasks; using system.windows; using system.windows.input; using microsoft.win32; using microsoft.windowsapicodepack.dialogs; // for modern folder browser namespace pdffilescanner { public class mainviewmodel : inotifypropertychanged { // inotifypropertychanged 实现,用于通知ui属性已更改 public event propertychangedeventhandler? propertychanged; protected virtual void onpropertychanged(string propertyname) { propertychanged?.invoke(this, new propertychangedeventargs(propertyname)); } // 存储pdf文件信息的集合,observablecollection能自动通知ui更新 public observablecollection<pdffileinfo> pdffiles { get; } = new observablecollection<pdffileinfo>(); private string _statustext = "请选择一个文件夹..."; public string statustext { get => _statustext; set { _statustext = value; onpropertychanged(nameof(statustext)); } } private double _progressvalue; public double progressvalue { get => _progressvalue; set { _progressvalue = value; onpropertychanged(nameof(progressvalue)); } } private bool _isbusy; public bool isbusy { get => _isbusy; set { _isbusy = value; onpropertychanged(nameof(isbusy)); // 当isbusy状态改变时,通知命令重新评估其能否执行 ((relaycommand)selectfoldercommand).raisecanexecutechanged(); ((relaycommand)exporttoexcelcommand).raisecanexecutechanged(); } } // 命令绑定 public icommand selectfoldercommand { get; } public icommand exporttoexcelcommand { get; } public mainviewmodel() { selectfoldercommand = new relaycommand(async () => await processfolderasync(), () => !isbusy); exporttoexcelcommand = new relaycommand(exporttoexcel, () => pdffiles.count > 0 && !isbusy); } private async task processfolderasync() { // 使用现代化的文件夹选择对话框 var dialog = new commonopenfiledialog { isfolderpicker = true, title = "请选择包含pdf文件的文件夹" }; if (dialog.showdialog() == commonfiledialogresult.ok) { string selectedpath = dialog.filename; isbusy = true; statustext = "正在准备处理..."; pdffiles.clear(); progressvalue = 0; await task.run(() => // 在后台线程执行耗时操作,避免ui卡死 { var files = directory.getfiles(selectedpath, "*.pdf"); int processedcount = 0; foreach (var file in files) { processedcount++; var progresspercentage = (double)processedcount / files.length * 100; // 更新ui元素必须在ui线程上执行 application.current.dispatcher.invoke(() => { statustext = $"正在处理: {path.getfilename(file)} ({processedcount}/{files.length})"; progressvalue = progresspercentage; }); try { // 获取文件信息 var fileinfo = new fileinfo(file); int pagecount = 0; // 使用 itext7 读取pdf页数 using (var pdfreader = new pdfreader(file)) { using (var pdfdoc = new pdfdocument(pdfreader)) { pagecount = pdfdoc.getnumberofpages(); } } // 创建模型对象并添加到集合中 var pdfdata = new pdffileinfo { filename = fileinfo.name, pagecount = pagecount, filesize = $"{fileinfo.length / 1024.0:f2} kb" // 格式化文件大小 }; application.current.dispatcher.invoke(() => pdffiles.add(pdfdata)); } catch (system.exception ex) { // 如果某个pdf文件损坏,记录错误并继续 application.current.dispatcher.invoke(() => { statustext = $"处理文件 {path.getfilename(file)} 时出错: {ex.message}"; }); } } }); statustext = $"处理完成!共找到 {pdffiles.count} 个pdf文件。"; isbusy = false; } } private void exporttoexcel() { var savefiledialog = new savefiledialog { filter = "excel 工作簿 (*.xlsx)|*.xlsx", filename = $"pdf文件列表_{system.datetime.now:yyyymmddhhmmss}.xlsx" }; if (savefiledialog.showdialog() == true) { try { // 使用 npoi 创建 excel iworkbook workbook = new xssfworkbook(); isheet sheet = workbook.createsheet("pdf文件信息"); // 创建表头 irow headerrow = sheet.createrow(0); headerrow.createcell(0).setcellvalue("文件名"); headerrow.createcell(1).setcellvalue("页数"); headerrow.createcell(2).setcellvalue("文件大小 (kb)"); // 填充数据 for (int i = 0; i < pdffiles.count; i++) { irow datarow = sheet.createrow(i + 1); datarow.createcell(0).setcellvalue(pdffiles[i].filename); datarow.createcell(1).setcellvalue(pdffiles[i].pagecount); datarow.createcell(2).setcellvalue(pdffiles[i].filesize); } // 自动调整列宽 sheet.autosizecolumn(0); sheet.autosizecolumn(1); sheet.autosizecolumn(2); // 写入文件 using (var fs = new filestream(savefiledialog.filename, filemode.create, fileaccess.write)) { workbook.write(fs); } messagebox.show("成功导出到excel!", "导出成功", messageboxbutton.ok, messageboximage.information); } catch (system.exception ex) { messagebox.show($"导出失败: {ex.message}", "错误", messageboxbutton.ok, messageboximage.error); } } } } // 一个简单的icommand实现 public class relaycommand : icommand { private readonly system.action _execute; private readonly system.func<bool>? _canexecute; public event system.eventhandler? canexecutechanged { add { commandmanager.requerysuggested += value; } remove { commandmanager.requerysuggested -= value; } } public relaycommand(system.action execute, system.func<bool>? canexecute = null) { _execute = execute; _canexecute = canexecute; } public bool canexecute(object? parameter) => _canexecute == null || _canexecute(); public void execute(object? parameter) => _execute(); public void raisecanexecutechanged() => commandmanager.invalidaterequerysuggested(); } }
第四步:设计用户界面 (view)
这是 mainwindow.xaml
文件,定义了程序窗口的布局和控件,并将它们绑定到 viewmodel。
mainwindow.xaml
<window x:class="pdffilescanner.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:pdffilescanner" mc:ignorable="d" title="pdf文件扫描器" height="600" width="800" minheight="400" minwidth="600"> <!-- 设置窗口的数据上下文为viewmodel --> <window.datacontext> <local:mainviewmodel/> </window.datacontext> <grid margin="10"> <grid.rowdefinitions> <rowdefinition height="auto"/> <rowdefinition height="*"/> <rowdefinition height="auto"/> </grid.rowdefinitions> <!-- 顶部操作栏 --> <stackpanel grid.row="0" orientation="horizontal" margin="0,0,0,10"> <button content="选择文件夹" command="{binding selectfoldercommand}" padding="15,5" fontsize="14" isenabled="{binding !isbusy}"/> <button content="导出到excel" command="{binding exporttoexcelcommand}" margin="10,0,0,0" padding="15,5" fontsize="14" isenabled="{binding !isbusy}"/> </stackpanel> <!-- 文件列表 --> <datagrid grid.row="1" itemssource="{binding pdffiles}" autogeneratecolumns="false" canuseraddrows="false" isreadonly="true" fontsize="14"> <datagrid.columns> <datagridtextcolumn header="文件名" binding="{binding filename}" width="*"/> <datagridtextcolumn header="页数" binding="{binding pagecount}" width="auto"/> <datagridtextcolumn header="文件大小" binding="{binding filesize}" width="auto"/> </datagrid.columns> </datagrid> <!-- 底部状态栏和进度条 --> <grid grid.row="2" margin="0,10,0,0"> <grid.columndefinitions> <columndefinition width="*"/> <columndefinition width="200"/> </grid.columndefinitions> <textblock grid.column="0" text="{binding statustext}" verticalalignment="center" texttrimming="characterellipsis"/> <progressbar grid.column="1" value="{binding progressvalue}" maximum="100" height="20" visibility="{binding isbusy, converter={staticresource booleantovisibilityconverter}}"/> </grid> </grid> </window>
mainwindow.xaml.cs (代码隐藏文件)
这里我们只需要确保 datacontext
被正确设置。上面的xaml已经通过 <local:mainviewmodel/>
标签完成了这一步,所以代码隐藏文件非常干净。
using system.windows; namespace pdffilescanner { public partial class mainwindow : window { public mainwindow() { initializecomponent(); // datacontext 在 xaml 中设置,这里无需代码 } } }
总结与解释
- 文件夹选择: 点击“选择文件夹”按钮,会触发
selectfoldercommand
。我们使用了microsoft.windowsapicodepack-shell
库,它提供了一个比默认的folderbrowserdialog
更现代、更友好的对话框。 - 后台处理与进度更新:
- 核心的pdf文件处理逻辑被包裹在
task.run()
中,这会将其放到一个后台线程上执行,防止ui线程(负责渲染窗口和响应用户操作的线程)被阻塞而导致程序“未响应”。 - 在后台线程中,我们不能直接修改ui控件(如
progressbar
或textblock
)或绑定到ui的集合(如observablecollection
)。因此,我们使用application.current.dispatcher.invoke()
将这些更新操作“派发”回ui线程执行,这是wpf中进行跨线程ui更新的标准做法。 isbusy
属性用来控制ui状态。当isbusy
为true
时,按钮会被禁用,进度条会显示。
- 核心的pdf文件处理逻辑被包裹在
- 信息提取:
- 文件名和大小: 使用
system.io.fileinfo
类可以轻松获取。 - pdf页数: 使用
itext 7
库。我们通过pdfreader
和pdfdocument
对象打开pdf文件,然后调用getnumberofpages()
方法。using
语句确保文件流被正确关闭和释放。
- 文件名和大小: 使用
- 列表展示:
- wpf的
datagrid
控件的itemssource
属性被绑定到 viewmodel 中的observablecollection<pdffileinfo>
集合。 observablecollection
的美妙之处在于,每当你向其中add
或remove
一个项时,它会自动通知绑定的datagrid
更新,无需手动刷新。
- wpf的
- excel导出:
- 点击“导出到excel”按钮会触发
exporttoexcelcommand
。 - 该命令首先会弹出一个标准的“文件保存”对话框,让用户选择保存位置和文件名。
- 然后,它使用 npoi 库在内存中创建一个excel工作簿 (
xssfworkbook
对应 .xlsx格式),创建工作表、表头行,然后遍历pdffiles
集合,将每条数据写入新的一行。 - 最后,将内存中的工作簿写入到用户选择的文件流中。
- 点击“导出到excel”按钮会触发
这个方案完整地实现了你要求的所有功能,并且采用了现代c#和wpf的最佳实践,代码结构清晰,易于扩展和维护。
后记
关于转换器的错误
visibility="{binding isbusy, converter={staticresource booleantovisibilityconverter}}"
改代码因没有转换器converter而出错,故需自定义一个转换器:
添加转换器类booleantovisibilityconverter
:
using system; using system.globalization; using system.windows; using system.windows.data; public class booleantovisibilityconverter : ivalueconverter { public object convert(object value, type targettype, object parameter, cultureinfo culture) { if (value is bool booleanvalue) { if (booleanvalue) { return visibility.visible; } else { // default to collapsed, or hidden based on 'parameter' or another property return visibility.collapsed; } } return visibility.visible; // default if not a boolean } public object convertback(object value, type targettype, object parameter, cultureinfo culture) { throw new notimplementedexception(); // usually not needed for visibility conversion } }
然后在 mainwindow.xaml
中注册这个转换器:
<!-- 在这里添加资源定义 --> <window.resources> <booleantovisibilityconverter x:key="booleantovisibilityconverter"/> </window.resources>
修改后的mainwindow.xaml
文件如下:
<window x:class="pdffilescanner.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:pdffilescanner" mc:ignorable="d" title="pdf文件扫描器" height="600" width="800" minheight="400" minwidth="600"> <!-- 设置窗口的数据上下文为viewmodel --> <window.datacontext> <local:mainviewmodel/> </window.datacontext> <!-- 在这里添加资源定义 --> <window.resources> <booleantovisibilityconverter x:key="booleantovisibilityconverter"/> </window.resources> <grid margin="10"> <grid.rowdefinitions> <rowdefinition height="auto"/> <rowdefinition height="*"/> <rowdefinition height="auto"/> </grid.rowdefinitions> <!-- 顶部操作栏 --> <stackpanel grid.row="0" orientation="horizontal" margin="0,0,0,10"> <button content="选择文件夹" command="{binding selectfoldercommand}" padding="15,5" fontsize="14" isenabled="{binding !isbusy}"/> <button content="导出到excel" command="{binding exporttoexcelcommand}" margin="10,0,0,0" padding="15,5" fontsize="14" isenabled="{binding !isbusy}"/> </stackpanel> <!-- 文件列表 --> <datagrid grid.row="1" itemssource="{binding pdffiles}" autogeneratecolumns="false" canuseraddrows="false" isreadonly="true" fontsize="14"> <datagrid.columns> <datagridtextcolumn header="文件名" binding="{binding filename}" width="*"/> <datagridtextcolumn header="页数" binding="{binding pagecount}" width="auto"/> <datagridtextcolumn header="文件大小" binding="{binding filesize}" width="auto"/> </datagrid.columns> </datagrid> <!-- 底部状态栏和进度条 --> <grid grid.row="2" margin="0,10,0,0"> <grid.columndefinitions> <columndefinition width="*"/> <columndefinition width="200"/> </grid.columndefinitions> <textblock grid.column="0" text="{binding statustext}" verticalalignment="center" texttrimming="characterellipsis"/> <progressbar grid.column="1" value="{binding progressvalue}" maximum="100" height="20" visibility="{binding isbusy, converter={staticresource booleantovisibilityconverter}}"/> </grid> </grid> </window>
问题解决!
运行效果如下:
以上就是c# wpf实现读取文件夹中的pdf并显示其页数的操作指南的详细内容,更多关于c# wpf读取文件夹pdf并显示其页数的资料请关注代码网其它相关文章!
发表评论