当前位置: 代码网 > it编程>编程语言>Asp.net > C# WPF实现读取文件夹中的PDF并显示其页数的操作指南

C# WPF实现读取文件夹中的PDF并显示其页数的操作指南

2025年09月24日 Asp.net 我要评论
工作中需要整理一些pdf格式文件,程序员的存在就是为了让大家可以“懒更高效地工作”,而ai的出现就可以让程序更“懒高效地工作”,于是求助于很长(我指上下

工作中需要整理一些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 中设置,这里无需代码
        }
    }
}

总结与解释

  1. 文件夹选择: 点击“选择文件夹”按钮,会触发 selectfoldercommand。我们使用了 microsoft.windowsapicodepack-shell 库,它提供了一个比默认的 folderbrowserdialog 更现代、更友好的对话框。
  2. 后台处理与进度更新:
    • 核心的pdf文件处理逻辑被包裹在 task.run() 中,这会将其放到一个后台线程上执行,防止ui线程(负责渲染窗口和响应用户操作的线程)被阻塞而导致程序“未响应”。
    • 在后台线程中,我们不能直接修改ui控件(如 progressbartextblock)或绑定到ui的集合(如 observablecollection)。因此,我们使用 application.current.dispatcher.invoke() 将这些更新操作“派发”回ui线程执行,这是wpf中进行跨线程ui更新的标准做法。
    • isbusy 属性用来控制ui状态。当 isbusytrue 时,按钮会被禁用,进度条会显示。
  3. 信息提取:
    • 文件名和大小: 使用 system.io.fileinfo 类可以轻松获取。
    • pdf页数: 使用 itext 7 库。我们通过 pdfreaderpdfdocument 对象打开pdf文件,然后调用 getnumberofpages() 方法。using 语句确保文件流被正确关闭和释放。
  4. 列表展示:
    • wpf的 datagrid 控件的 itemssource 属性被绑定到 viewmodel 中的 observablecollection<pdffileinfo> 集合。
    • observablecollection 的美妙之处在于,每当你向其中 addremove 一个项时,它会自动通知绑定的 datagrid 更新,无需手动刷新。
  5. excel导出:
    • 点击“导出到excel”按钮会触发 exporttoexcelcommand
    • 该命令首先会弹出一个标准的“文件保存”对话框,让用户选择保存位置和文件名。
    • 然后,它使用 npoi 库在内存中创建一个excel工作簿 (xssfworkbook 对应 .xlsx格式),创建工作表、表头行,然后遍历 pdffiles 集合,将每条数据写入新的一行。
    • 最后,将内存中的工作簿写入到用户选择的文件流中。

这个方案完整地实现了你要求的所有功能,并且采用了现代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并显示其页数的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com