C# WPF實現(xiàn)讀取文件夾中的PDF并顯示其頁數(shù)的操作指南
工作中需要整理一些PDF格式文件,程序員的存在就是為了讓大家可以“懶更高效地工作”,而AI的出現(xiàn)就可以讓程序更“懶高效地工作”,于是求助于很長(我指上下文)的Gemini,它幫助了我快速搭建項目,但也給我留下了坑(見本文“后記”部分),于是我把這個開發(fā)過程記錄了下來。
技術(shù)選型
- UI框架: WPF (.NET 6/7/8 或 .NET Framework 4.7.2+) - 用于構(gòu)建現(xiàn)代化的Windows桌面應用。
- PDF處理: iText (替代了舊版的 iTextSharp 及 iText7) - 一個強大且流行的開源PDF處理庫。
- Excel導出: NPOI - 一個開源的.NET庫,可以讀寫Office文檔,無需安裝Microsoft Office。
- 設(shè)計模式: MVVM - 使UI和業(yè)務(wù)邏輯分離,提高代碼的可測試性和復用性。
第一步:創(chuàng)建項目并安裝依賴庫
打開 Visual Studio,創(chuàng)建一個新的 WPF 應用程序 項目(本文為.net 8.0項目)。

通過 NuGet 包管理器安裝以下必要的庫。在“解決方案資源管理器”中右鍵點擊你的項目,選擇“管理NuGet程序包”,然后搜索并安裝:
iTextNPOIMicrosoft.WindowsAPICodePack-Shell(為了一個更好看的文件夾選擇對話框)

第二步:定義數(shù)據(jù)模型 (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;
}
}
第三步:創(chuàng)建視圖模型 (ViewModel)
ViewModel 是連接視圖和模型的橋梁,包含了所有的業(yè)務(wù)邏輯和UI狀態(tài),在這里,我按照AI的提示創(chuàng)建了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 實現(xiàn),用于通知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狀態(tài)改變時,通知命令重新評估其能否執(zhí)行
((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()
{
// 使用現(xiàn)代化的文件夾選擇對話框
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(() => // 在后臺線程執(zhí)行耗時操作,避免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線程上執(zhí)行
Application.Current.Dispatcher.Invoke(() =>
{
StatusText = $"正在處理: {Path.GetFileName(file)} ({processedCount}/{files.Length})";
ProgressValue = progressPercentage;
});
try
{
// 獲取文件信息
var fileInfo = new FileInfo(file);
int pageCount = 0;
// 使用 iText7 讀取PDF頁數(shù)
using (var pdfReader = new PdfReader(file))
{
using (var pdfDoc = new PdfDocument(pdfReader))
{
pageCount = pdfDoc.GetNumberOfPages();
}
}
// 創(chuàng)建模型對象并添加到集合中
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文件損壞,記錄錯誤并繼續(xù)
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 創(chuàng)建 Excel
IWorkbook workbook = new XSSFWorkbook();
ISheet sheet = workbook.CreateSheet("PDF文件信息");
// 創(chuàng)建表頭
IRow headerRow = sheet.CreateRow(0);
headerRow.CreateCell(0).SetCellValue("文件名");
headerRow.CreateCell(1).SetCellValue("頁數(shù)");
headerRow.CreateCell(2).SetCellValue("文件大小 (KB)");
// 填充數(shù)據(jù)
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);
}
// 自動調(diào)整列寬
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實現(xiàn)
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();
}
}
第四步:設(shè)計用戶界面 (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">
<!-- 設(shè)置窗口的數(shù)據(jù)上下文為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="頁數(shù)" Binding="{Binding PageCount}" Width="Auto"/>
<DataGridTextColumn Header="文件大小" Binding="{Binding FileSize}" Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
<!-- 底部狀態(tài)欄和進度條 -->
<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 被正確設(shè)置。上面的XAML已經(jīng)通過 <local:MainViewModel/> 標簽完成了這一步,所以代碼隱藏文件非常干凈。
using System.Windows;
namespace PdfFileScanner
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// DataContext 在 XAML 中設(shè)置,這里無需代碼
}
}
}
總結(jié)與解釋
- 文件夾選擇: 點擊“選擇文件夾”按鈕,會觸發(fā)
SelectFolderCommand。我們使用了Microsoft.WindowsAPICodePack-Shell庫,它提供了一個比默認的FolderBrowserDialog更現(xiàn)代、更友好的對話框。 - 后臺處理與進度更新:
- 核心的PDF文件處理邏輯被包裹在
Task.Run()中,這會將其放到一個后臺線程上執(zhí)行,防止UI線程(負責渲染窗口和響應用戶操作的線程)被阻塞而導致程序“未響應”。 - 在后臺線程中,我們不能直接修改UI控件(如
ProgressBar或TextBlock)或綁定到UI的集合(如ObservableCollection)。因此,我們使用Application.Current.Dispatcher.Invoke()將這些更新操作“派發(fā)”回UI線程執(zhí)行,這是WPF中進行跨線程UI更新的標準做法。 IsBusy屬性用來控制UI狀態(tài)。當IsBusy為true時,按鈕會被禁用,進度條會顯示。
- 核心的PDF文件處理邏輯被包裹在
- 信息提取:
- 文件名和大小: 使用
System.IO.FileInfo類可以輕松獲取。 - PDF頁數(shù): 使用
iText 7庫。我們通過PdfReader和PdfDocument對象打開PDF文件,然后調(diào)用GetNumberOfPages()方法。using語句確保文件流被正確關(guān)閉和釋放。
- 文件名和大小: 使用
- 列表展示:
- WPF的
DataGrid控件的ItemsSource屬性被綁定到 ViewModel 中的ObservableCollection<PdfFileInfo>集合。 ObservableCollection的美妙之處在于,每當你向其中Add或Remove一個項時,它會自動通知綁定的DataGrid更新,無需手動刷新。
- WPF的
- Excel導出:
- 點擊“導出到Excel”按鈕會觸發(fā)
ExportToExcelCommand。 - 該命令首先會彈出一個標準的“文件保存”對話框,讓用戶選擇保存位置和文件名。
- 然后,它使用 NPOI 庫在內(nèi)存中創(chuàng)建一個Excel工作簿 (
XSSFWorkbook對應 .xlsx格式),創(chuàng)建工作表、表頭行,然后遍歷PdfFiles集合,將每條數(shù)據(jù)寫入新的一行。 - 最后,將內(nèi)存中的工作簿寫入到用戶選擇的文件流中。
- 點擊“導出到Excel”按鈕會觸發(fā)
這個方案完整地實現(xiàn)了你要求的所有功能,并且采用了現(xiàn)代C#和WPF的最佳實踐,代碼結(jié)構(gòu)清晰,易于擴展和維護。
后記
關(guān)于轉(zhuǎn)換器的錯誤
Visibility="{Binding IsBusy, Converter={StaticResource BooleanToVisibilityConverter}}" 改代碼因沒有轉(zhuǎn)換器Converter而出錯,故需自定義一個轉(zhuǎn)換器:
添加轉(zhuǎn)換器類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 中注冊這個轉(zhuǎn)換器:
<!-- 在這里添加資源定義 -->
<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">
<!-- 設(shè)置窗口的數(shù)據(jù)上下文為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="頁數(shù)" Binding="{Binding PageCount}" Width="Auto"/>
<DataGridTextColumn Header="文件大小" Binding="{Binding FileSize}" Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
<!-- 底部狀態(tài)欄和進度條 -->
<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實現(xiàn)讀取文件夾中的PDF并顯示其頁數(shù)的操作指南的詳細內(nèi)容,更多關(guān)于C# WPF讀取文件夾PDF并顯示其頁數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#使用StringBuilder實現(xiàn)高效處理字符串
這篇文章主要為大家詳細介紹了C#如何使用StringBuilder實現(xiàn)高效處理字符串,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2024-01-01
DevExpress之TreeList用法實例總結(jié)
這篇文章主要介紹了DevExpress之TreeList用法,對于C#初學者有一定的借鑒價值,需要的朋友可以參考下2014-08-08

