使用 itextsharp 的 PDF 分割和合并工具






4.31/5 (13投票s)
使用 itextsharp 的 PDF 分割和合并工具
引言
我最近参加了 70-511 WPF 课程,我想与大家分享我学到的技能。在本文中,我将介绍我使用 Visual Studio 2015 在 WPF 中创建的一个 PDF 分割和合并工具。
最新的代码可以在我的 Github 仓库 这里 找到,Windows 安装程序可以在 这里 找到
ClickOnce 部署可以 这里 启动。
背景
受多个商业产品的启发,我决定创建一个 PDF 分割和合并工具。该工具使用了 iTextSharp 库,该库允许您创建和修改便携式文档格式 (PDF) 的文档。PDF 分割和合并工具的源代码可以在本文的顶部找到。下面的两个截图展示了应用程序的用户界面,简洁易用。
应用程序架构
PDF 分割和合并工具是使用 MVVM 模式创建的。启动时,会创建 MainView
并设置其 DataContext
,然后将 MainView
显示给用户。
protected override void OnStartup(StartupEventArgs e)
{
try
{
MainView mainView = new MainView();
mainView.DataContext = new MainViewModel();
mainView.Show();
}
catch (Exception ex)
{
Debug.WriteLine("OnStartup" + ex.ToString());
}
}
MainView
包含一个 TabControl
,该控件的每个 TabItem
都绑定到一个 ViewModel
。DataTemplate
用于渲染(告诉 WPF 如何绘制)每个特定 UserControl
的 ViewModel
。这种方法使业务逻辑(ViewModels
)与 UI(Views)完全分离。TabControl
在 MainView
中定义如下
<TabControl ItemsSource="{Binding TabControls, Mode=OneWay}"
SelectedItem="{Binding SelectedTab,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:SplitPdfViewModel}">
<v:SplitPdfView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MergePdfViewModel}">
<v:MergePdfView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:AboutViewModel}">
<v:AboutView />
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}" >
<Setter Property="Header"
Value="{Binding Header}"></Setter>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
TabControl ItemsSource
绑定到一个 ViewModels
集合。该集合在 MainViewModel
中创建。每个 ViewModel
对象都是 TabItem
的 DataContext
。SelectedTab
属性用于跟踪哪个 TabItem
被选中。
public MainWindowViewModel()
{
InitializeProperties();
}
private void InitializeProperties()
{
TabControls = new ObservableCollection<ITabViewModel>();
TabControls.Add(new SplitPdfViewModel("Split"));
TabControls.Add(new MergePdfViewModel("Merge"));
TabControls.Add(new AboutViewModel("About"));
SelectedTab = TabControls.First();
}
private ITabViewModel selectedTab = null;
public ITabViewModel SelectedTab
{
get { return selectedTab; }
set { SetProperty(ref selectedTab, value); }
}
private ObservableCollection<ITabViewModel> tabControls = null;
public ObservableCollection<ITabViewModel> TabControls
{
get { return tabControls; }
private set { SetProperty(ref tabControls, value); }
}
每个 TabItem ViewModel
都需要实现 ITabViewModel
接口。该接口要求 ViewModel
实现一个 Header
属性,该属性用于 TabItem
的 Header
属性。
public interface ITabViewModel
{
string Header { get; set; }
}
当用户选择一个密码保护的 PDF 文件时,DataGrid
中会出现一个锁图标,而不是绿色的勾图标。用户可以单击此图标,然后会出现一个基于 MVVM 的模态 PDF 解锁对话框,提示用户输入密码以解锁 PDF 文件。模态 PDF 解锁对话框是 MainView
的子窗口,需要您输入密码才能返回 MainView
。
模态 PDF 解锁对话框创建如下
private void OnUnlockCmdExecute()
{
PdfLoginView pdfLoginView = new PdfLoginView();
pdfLoginView.DataContext = new PdfLoginViewModel(pdfLoginView, PdfFiles.First());
pdfLoginView.Owner = App.Current.MainWindow;
bool? dialogResult = pdfLoginView.ShowDialog();
if (dialogResult.HasValue && dialogResult.Value)
{
ProgressStatus = "PDF file successfully unlocked.";
}
else
{
ProgressStatus = "Failed to unlock PDF file.";
}
}
请注意,LoginView
被传递给了 LoginViewModel
的构造函数。这样,LoginViewModel
就拥有了 LoginView
的引用,可以设置 LoginView
的 dialogResult
并关闭它,如下面的代码所示。
private void OnOkCmdExecute(object parameter)
{
if (LockedFile.Open(((System.Windows.Controls.PasswordBox)parameter).Password) == Define.Success)
{
HasError = false;
ErrorContent = string.Empty;
pdfLoginView.DialogResult = true;
pdfLoginView.Close();
}
else
{
((System.Windows.Controls.PasswordBox)parameter).Password = string.Empty;
ErrorContent = "Failed to unlock PDF file, please try again.";
HasError = true;
}
}
除了模态解锁对话框,PDF 分割和合并工具还使用两个非模态对话框。非模态对话框用于显示对应用程序的继续不重要的信息,从而允许对话框保持打开状态,同时应用程序继续工作。当用户双击 DataGrid
中的 PDF 条目,或者右键单击 DataGrid
中的 PDF 条目并选择“显示 PDF 文件属性”时,会使用非模态对话框来显示 PDF 文件属性。第二个非模态对话框用于在 WebBrowser
控件中打开 PDF 文件。您可以通过右键单击 DataGrid
中的 PDF 条目并选择“打开 PDF 文件”从上下文菜单中打开 PDF 文件。打开和关闭非模态对话框的代码如下所示。
private void OnShowFilePropertiesCmdExecute()
{
Dictionary<string, string> properties = null;
Dictionary<string, string> security = null;
Dictionary<string, string> info = null;
if (SelectedFile.GetProperties(out properties, out info, out security) == Define.Success)
{
FilePropertiesViewModel filePropertiesViewModel =
new FilePropertiesViewModel(SelectedFile.Info.FullName,
properties, info, security);
FilePropertiesView propertiesView = new FilePropertiesView();
propertiesView.DataContext = filePropertiesViewModel;
propertiesView.Owner = App.Current.MainWindow;
propertiesView.Show();
}
}
为了从 FilePropertiesViewModel
关闭 FilePropertiesView
,FilePropertiesView
将其引用作为命令参数传递给关闭命令。
<Window.InputBindings>
<KeyBinding Key="Escape"
Command="{Binding CloseWindowCmd}"
CommandParameter="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
</Window.InputBindings>
FilePropertiesViewModel
中的关闭命令实现接收 FilePropertiesView
窗口作为对象参数,然后可以使用该参数来关闭它。请注意,此方法是模态解锁对话框中关闭过程的一种替代方法。
public void OnCloseWindowCmdExecute(object parameter)
{
SystemCommands.CloseWindow((Window)parameter);
}
正如本文顶部的截图所示,MainView
实现了一个状态栏。状态栏包含一个进度条和一个标签。状态栏项绑定到选定的 TabItem
。对于“关于”TabItem
,在 AboutViewModel
中将状态栏的可见性设置为隐藏。
<StatusBar DockPanel.Dock="Bottom"
VerticalAlignment="Bottom" Background="LightGray">
<StatusBarItem Visibility="{Binding Path=SelectedTab.StatusBarIsVisible}">
<Grid>
<ProgressBar x:Name="pbStatus"
Width="100"
Height="20"
Value="{Binding Path=SelectedTab.ProgressBarValue, Mode=OneWay}"
IsIndeterminate="{Binding Path=SelectedTab.ProgressBarIsIndeterminate,
Mode=OneWay}"/>
<TextBlock Text="{Binding ElementName=pbStatus,
Path=Value, StringFormat={}{0:0}%}"
HorizontalAlignment="Center"
VerticalAlignment="Center"></TextBlock>
</Grid>
</StatusBarItem>
<Separator Visibility="
{Binding Path=SelectedTab.StatusBarIsVisible}"></Separator>
<StatusBarItem Visibility="{Binding Path=SelectedTab.StatusBarIsVisible}">
<Label Content="{Binding Path=SelectedTab.ProgressStatus}"
HorizontalContentAlignment="Left"
VerticalAlignment="Bottom"
MinWidth="200"
Height="26"></Label>
</StatusBarItem>
</StatusBar>
我使用 RelayCommand
来调用 ViewModel
中的命令。RelayCommand
是一个实现了 ICommand
接口的自定义命令类。
public class RelayCommand : ICommand {}
RelayCommand
使您能够使用集中的任务架构。您可以将任意数量的 UI 控件或输入手势与 RelayCommand
关联,并将该 RelayCommand
绑定到一个处理程序,该处理程序在控件激活或执行手势时执行。RelayCommand
还会跟踪它们是否可用。如果 RelayCommand
被禁用,与之关联的 UI 元素也将被禁用。下面通过实现拆分命令来说明 RelayCommand
的用法。OnSplitPdfCmdCanExecute
仅在 IsBusy
标志为 false
时返回 true
,从而启用拆分命令。OnSplitPdfCmdExecute
是包含拆分逻辑的处理程序。执行拆分命令时会调用此处理程序。
public RelayCommand SplitPdfCmd { get; private set; }
SplitPdfCmd = new RelayCommand(OnSplitPdfCmdExecute, OnSplitPdfCmdCanExecute);
private bool OnSplitPdfCmdCanExecute()
{
return !IsBusy;
}
private void OnSplitPdfCmdExecute()
{
PageRangeParser pageRangeParser = null;
IsBusy = true;
bool isValid = ViewIsValid(out pageRangeParser);
if (isValid)
{
splitBackgroundWorker.RunWorkerAsync(new object[] { PdfFiles.First(),
pageRangeParser, DestinationPath, OverwriteFile });
}
else
{
IsBusy = false;
}
}
关于架构的最后一个注意事项是,应用程序中的所有 ViewModel
都继承自 ViewModelBase
。ViewModelBase
是一个基类,包含应用程序中不同 ViewModel
可以使用的通用方法,它实现了 IDisposable
、INotifyPropertyChanged
和 INotifyDataErrorInfo
接口。通过使用此类,可以避免重复代码并提高可维护性。
控件和布局
MainView
使用 DockPanel
控件来布局其控件。DockPanel
控件是一个容器,允许您将包含的控件停靠到停靠面板的边缘。它通过提供一个名为 Dock
的附加属性来实现对包含控件的停靠。以下代码演示了如何使用 DockPanel
设置 MainView 的布局,实现细节未显示,可在包含的代码中找到。
<DockPanel LastChildFill="True">
<Border DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal">
<Image/>
<TextBlock/>
<TextBlock/>
</StackPanel>
</Border>
<StatusBar DockPanel.Dock="Bottom"></StatusBar>
<TabControl></TabControl>
</DockPanel>
DockPanel.Dock
属性有四个可能的值:Top
、Bottom
、Left
和 Right
,分别表示停靠到 DockPanel
控件的顶部、底部、左侧和右侧边缘。DockPanel
控件公开了一个名为 LastChildFill
的属性,可以将其设置为 True
或 False
。当设置为 True
(默认设置)时,添加到布局中的最后一个控件将填充所有剩余空间。因此,MainView
的布局如下:它有一个包含标题文本的标题。标题文本嵌入在停靠在 MainView
顶部(DockPanel.Dock="Top"
)的边框控件中。状态栏停靠在 MainView
的底部(DockPanel.Dock="Bottom"
),而 TabControl
将填充所有剩余空间,因为它是在 DockPanel
中添加的最后一个子控件。
拆分、合并和关于视图使用 Grid
来布局所有控件。Grid
是 WPF 中创建用户界面最常用的面板。使用 Grid
控件,您可以在 Grid
中定义列和行。然后,您可以将子控件分配给指定的行和列,以创建更具结构化的布局。合并视图包含一个 GridSplitter
控件,允许用户在运行时调整 Grid
行的大小。使用 GridSplitter
,用户可以通过用鼠标抓住包含 GridSplitter
的行并移动它来调整 Grid 行的大小,从而展开包含 PdfFile
条目的 DataGrid
。
<GridSplitter Grid.Row="1"
Height="5"
Width="Auto"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ResizeBehavior="PreviousAndNext"
ResizeDirection="Rows"></GridSplitter>
拆分和合并视图中的 PdfFile
条目显示在 DataGrid
中。这是通过将 PdfFile
对象的可观察集合绑定到 DataGrid
来实现的。PDF 分割和合并工具可以合并任意数量的 PDF 文件,但一次只能分割一个 PDF 文件。因此,合并视图中的 DataGrid
可以包含任意数量的 PdfFile
条目,而拆分视图中的 DataGrid
只能包含一个 PdfFile
条目。
用于输入页面分割间隔的 IntegerTextBox
是一个继承自 TextBox
的自定义控件。它检查输入的值是否可以转换为无符号 16 位整数,如果可以,则接受输入,否则通过将 OnPreviewTextInput
隧道事件设置为已处理来忽略输入,从而阻止该事件的进一步隧道和冒泡。
拆分视图中的每个单选按钮都通过转换器绑定到一个 enum
属性。对于每个 enum
值,组中的特定单选按钮会被选中。指定分割方法的两个单选按钮绑定到 SplitMethod
enum
属性,下面显示了 enum
属性的定义
public enum DocSplitMethod
{
None,
Interval,
Range,
}
private DocSplitMethod splitMethod = DocSplitMethod.None;
public DocSplitMethod SplitMethod
{
get { return splitMethod; }
set { SetProperty(ref splitMethod, value); }
}
下面显示了指定分割方法的单选按钮的实现。通过 EnumToBoolConverter
和 ConverterParameter
可以将 enum
值转换为单选按钮的选中状态。
<RadioButton x:Name="rBtnRange"
ToolTip="Split PDF document into files containing different ranges of pages per file"
GroupName="SplitMethod">
<RadioButton.IsChecked>
<Binding Path="SplitMethod"
Mode="TwoWay"
UpdateSourceTrigger="PropertyChanged"
ConverterParameter="{x:Static root:DocSplitMethod.Range}">
<Binding.Converter>
<converter:EnumToBoolConverter></converter:EnumToBoolConverter>
</Binding.Converter>
</Binding>
</RadioButton.IsChecked>
<StackPanel>
<AccessText Text="By page _range"></AccessText>
<Label Content="_Page range separated by comma, semicolon to separate output files"
Target="{Binding ElementName=txtRange}"></Label>
</StackPanel>
</RadioButton>
下面显示了 enum
到 bool
的转换器。正如您在转换器的实现中看到的,如果传递的 ConverterParameter
等于绑定的 enum
值,Convert
方法将返回 true
,从而将相应单选按钮的 IsChecked
属性设置为 true
。
[ValueConversion(typeof(bool), typeof(Enum))]
public class EnumToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null || parameter == null)
return false;
else
return value.Equals(parameter);
}
public object ConvertBack(object value,
Type targetTypes, object parameter, CultureInfo culture)
{
if (value == null || parameter == null)
return false;
else if ((bool)value)
return parameter;
else
return Binding.DoNothing;
}
}
为了使合并 DataGrid
更用户友好,我添加了一个为每个 PDF 条目显示行号的行为。使用行号,您可以轻松地在合并 DataGrid
中查看和重新排列 PDF 条目。
Backgroundworker
分割和合并过程会消耗大量 CPU 时间。backgroundworker
提供了一种简单的方法来在后台运行这些过程,从而使用户界面保持响应并可供用户输入。SplitViewModel
使用两个 backgroundworker
,第一个用于加载 PDF 文件。第二个工作程序用于 PDF 分割过程。与 SplitViewModel
类似,MergeViewModel
也使用两个 backgroundworker
,一个用于加载 PDF 文件,另一个用于合并过程。SplitViewModel
中 backgroundworker
的初始化如下所示
private void InitializeBackgroundWorker()
{
splitBackgroundWorker = new BackgroundWorker();
fileBackgroundWorker = new BackgroundWorker();
fileBackgroundWorker.DoWork += new DoWorkEventHandler(FileWorkerDoWork);
fileBackgroundWorker.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(FileWorkerRunWorkerCompleted);
splitBackgroundWorker.WorkerReportsProgress = true;
splitBackgroundWorker.WorkerSupportsCancellation = true;
splitBackgroundWorker.DoWork +=
new DoWorkEventHandler(SplitWorkerDoWork);
splitBackgroundWorker.ProgressChanged +=
new ProgressChangedEventHandler(SplitWorkerProgressChanged);
splitBackgroundWorker.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(SplitWorkerRunWorkerCompleted);
}
加载 PDF 文件的 backgroundworker
实现两个事件处理程序,捕获 DoWork
和 RunWorkerCompleted
事件。在 DoWork
处理程序中,会创建一个 PdfFile
对象并设置其属性,然后由 backgroundworker
触发 RunWorkerCompleted
事件。RunWorkerCompleted
处理程序会更新状态栏并显示文件加载过程中发生的错误。执行文件分割的 backgroundworker
比加载 PDF 文件的 backgroundworker
多实现一个事件处理程序。在上面的代码中,您还可以看到拆分 backgroundworker
支持取消。通过在分割过程中单击取消按钮或按 Escape 键,会调用 CancelAsync
方法。此调用会返回 true
并设置拆分 backgroundworker 的 CancellationPending
属性。通过在 DoWork
处理程序中轮询 CancellationPending
属性,拆分逻辑可以确定何时取消拆分操作。在分割过程中,会定期触发 ProgressChanged
事件以更新进度状态。当拆分工作程序完成后,会调用 RunWorkerCompleted
事件处理程序,该处理程序会更新状态并显示分割过程中发生的错误。
PdfFile
集合由两个不同的线程访问,第一个是 UI 线程,第二个线程是 backgroundworker
。由于多个线程访问同一个集合,我使用了 ObservableCollection
并将负责更新集合的代码封装在 Dispatcher.Invoke
中。
拖放支持
PDF 分割和合并工具允许您将文件拖放到 DataGrid
中。此功能实现为一个行为,该行为有一个依赖项属性,指示是否允许多个 PDF 文件被拖放到 DataGrid
中。此依赖项属性是必需的,因为拆分 DataGrid
只允许拖放一个 PDF 文件,而合并 DataGrid
允许拖放多个 PDF 文件。拖放功能通过处理以下事件来实现
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.DragEnter += AssociatedObject_DragEnter;
this.AssociatedObject.DragLeave += AssociatedObject_DragLeave;
this.AssociatedObject.DragOver += AssociatedObject_DragOver;
this.AssociatedObject.Drop += AssociatedObject_Drop;
}
要启用拖放功能,必须将拆分和合并视图中 DataGrid
的 AllowDrop
属性设置为 true
。如前所述,拆分视图中的 DataGrid
只接受一个 PDF 文件,因为这个限制,当 DataGrid
包含一个 PDF 文件(SelectedFile != null
)时,AllowDrop
属性被设置为 false
。
<DataGrid.AllowDrop>
<Binding Path="SelectedFile" Mode="OneWay">
<Binding.Converter>
<converter:NullToBoolValueConverter/>
</Binding.Converter>
</Binding>
</DataGrid.AllowDrop>
当鼠标进入启用了拖放功能的 DataGrid
时,会触发 DragEnter
事件。此 DragEnter
事件将 DragEventArgs
对象传递给处理它的方法,并且可以检查 DragEventArgs
以查看正在拖到 DataGrid
上的 DataObject
是否是 PDF 文件以及源是否允许复制数据。如果数据确实是 PDF 文件并且允许复制数据,则拖放效果将设置为复制。以下示例演示了如何检查 DataObject
的数据格式并设置 Effect
属性
void AssociatedObject_DragEnter(object sender, DragEventArgs e)
{
e.Effects = DropAllowed(e);
e.Handled = true;
}
private DragDropEffects DropAllowed(DragEventArgs e)
{
DragDropEffects dragDropEffects = DragDropEffects.None;
if (e.Data.GetDataPresent(DataFormats.FileDrop) &&
(e.AllowedEffects & DragDropEffects.Copy) == DragDropEffects.Copy)
{
string[] Dropfiles = (string[])e.Data.GetData(DataFormats.FileDrop);
if ((AllowMultipleFiles && Dropfiles.Length > 0) ||
(!AllowMultipleFiles && Dropfiles.Length == 1))
{
int fileCnt = 0;
dragDropEffects = DragDropEffects.Copy;
do
{
if (string.Compare(GetExtension(Dropfiles[fileCnt]).ToLower(), ".pdf") != 0)
dragDropEffects = DragDropEffects.None;
} while (++fileCnt < Dropfiles.Length &&
dragDropEffects == DragDropEffects.Copy);
}
}
return dragDropEffects;
}
当对象拖动到 DataGrid
上时,会发生 DragOver
事件。此事件的处理程序接收一个 DragEventArgs
对象。在本例中,DragOver
事件用于通过将鼠标指针下方的 DataGrid
行的 IsSelected
属性设置为 true
来突出显示潜在的放置位置。
void AssociatedObject_DragOver(object sender, DragEventArgs e)
{
if (DropAllowed(e) == DragDropEffects.Copy)
{
var row = UIHelpers.TryFindFromPoint<DataGridRow>((DataGrid)sender,
e.GetPosition(AssociatedObject));
if (row != null) { ((DataGridRow)row).IsSelected = true; }
e.Effects = DragDropEffects.Copy;
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true;
}
在拖放操作期间,当鼠标按钮在目标控件上释放时,会引发 DragDrop
事件。该事件的处理程序接收一个 DragEventArgs
对象,使用 GetData
方法检索复制的数据并将其传递给实现 IDropable
接口的相应 ViewModel
。除了文件路径之外,还会将放置发生的行索引传递给 ViewModel
。这允许合并 ViewModel
在 PDF 文件集合(ObservableCollection<PdfFile>
)的特定位置追加 PDF 文件条目。
void AssociatedObject_Drop(object sender, DragEventArgs e)
{
IDropable target = this.AssociatedObject.DataContext as IDropable;
int index = -1;
if (target != null)
{
if (((DataGrid)sender) != null) { index = ((DataGrid)sender).SelectedIndex; }
target.Drop(e.Data.GetData(DataFormats.FileDrop), index);
}
e.Handled = true;
}
iTextSharp 库
您可以使用 Visual Studio 2015 中的 Nuget 包管理器安装 iTextSharp,可以在顶部菜单的 Tools->Nuget Package Manager 中找到。iTextSharp 库有完善的文档,您可以在其中找到许多使用该库的代码示例。在使用该库时,我发现如果释放一个空的 PDF 文档,库中会抛出一个错误。这是库中的一个已知 bug,因为错误只发生在用户取消分割或合并过程时,我通过在用户取消分割或合并过程时跳过清理来实现了一个变通方法。
if (destinationDoc != null &&
!splitBackgroundWorker.CancellationPending)
{
destinationDoc.Close();
destinationDoc.Dispose();
destinationDoc = null;
}
iTextSharp 库中引发错误的这行代码
if (pages.Count == 0)
{
throw new IOException("The document has no pages.")
};
如前所述,需要先解锁受保护的 PDF 文件,然后才能由 PDF 合并和分割工具进行处理。有两种不同类型的密码可用于保护 PDF 文件。用户密码,如果设置了,此密码是打开 PDF 所需提供的密码。所有者密码是在您想要为 PDF 应用权限时指定的密码。例如,如果您不想允许打印 PDF 或不允许提取页面,那么您可以指定防止这种情况发生的权限。但是,当为 iTextSharp PdfReader
设置 unethicalreading
标志为 true
时,PdfReader
会完全忽略安全设置,并且在未设置用户密码时会自动处理 PDF。
PdfReader.unethicalreading = true;
因此,PDF 分割和合并工具可以处理任何没有设置用户密码的 PDF 文件。此外,它还可以处理提供用户密码或所有者密码的任何 PDF 文件。
验证
分割和合并视图中的验证使用 INotifyDataErrorInfo
接口执行。验证错误使用基于 Silverlight 的错误模板显示给用户。有关 INotifyDataErrorInfo
接口和基于 Silverlight 的错误模板的详细信息,请参阅 此处。输入控件的验证是条件性进行的,这意味着只验证已启用的控件。
验证的一个挑战是验证输入的页码范围。每个指定的页码范围会生成一个 PDF 导出文件。页码范围可以通过两种不同的方式指定:闭合页码范围,其中起始页和结束页用破折号分隔(例如 1-7 或 23-8)。第二种指定页码范围的方式是通过逗号分隔的页码(例如 3,4,5,6 或 2,45,23,33 或仅一页 66)。在指定页码范围时,您可以根据需要组合这两种表示法,只需用“;”字符分隔每个页码范围即可。例如,以下页码范围是有效的:(1-7;3,4,5,6,1;8;9-5;3-5;4,5,1,2;)。通过使用正则表达式,我能够验证页码范围。生成的表达式如下所示
@"^((\d{1,4}-\d{1,4};)*|((\d{1,4},)*(\d{1,4};))*)*(((\d{1,4},)*(\d{1,4}))|(\d{1,4}-\d{1,4})){1};?$"
where:
'^' and '$' = Indicate respectively the start and end of the input line.
\d{1,4} = Matches an integer in the range 0 to 9999.
(\d{1,4}-\d{1,4};)* = Closed page range "d-d;" can occur 0 or more times.
((\d{1,4},)*(\d{1,4};))* = Comma separated page numbers.
(((\d{1,4},)*(\d{1,4}))|(\d{1,4}-\d{1,4})){1};?$ = makes the ';' char optional at the end
通过检查输入的页码范围和页面间隔是否包含超出 PDF 文档末尾的页码或零,也可以对其进行验证。如果包含,用户将收到验证错误提示,要求调整输入的值。
出于说明目的,我为 LoginView
创建了一个自定义验证。当用户输入错误的密码来解锁 PDF 时,HasError
属性会被设置为 true
,ErrorContent
也会被设置。在 LoginView
中,错误会显示给用户。
关于验证的最后一点是,在分割和合并过程中,Grid 会被禁用,使用 datatrigger
。这样,当应用程序忙碌时,就无法更改任何输入。
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="IsEnabled" Value="True"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter Property="IsEnabled" Value="False"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
键盘快捷键
键盘快捷键用于使应用程序对键盘友好。当 DataGrid
获得焦点时,可以使用以下键盘快捷键。
<DataGrid.InputBindings>
<KeyBinding Command="{Binding AddFileCmd}"
Key="Insert"/>
<KeyBinding Command="{Binding RemoveFileCmd}"
Key="Delete" />
<KeyBinding Command="{Binding UnlockCmd}"
Key="U" Modifiers="Control" />
<KeyBinding Command="{Binding ShowFilePropertiesCmd}"
Key="P" Modifiers="Control"/>
<KeyBinding Command="{Binding OpenFileCmd}"
Key="O" Modifiers="Control"/>
<KeyBinding Command="{Binding MoveUpCmd}"
Modifiers="Control" Key="Up" />
<KeyBinding Command="{Binding MoveDownCmd}"
Modifiers="Control" Key="Down" />
<KeyBinding Command="{Binding ScrollUpCmd}"
Key="Up"/>
<KeyBinding Command="{Binding ScrollDownCmd}"
Key="Down"/>
</DataGrid.InputBindings>
除了键盘快捷键,我还使用了助记键来使应用程序对键盘友好。标签控件支持助记键,当按下 Alt 键和助记键时,会将焦点转移到指定的控件。助记键通过在所需键前加上下划线 (_) 符号来指定,并在运行时按下 Alt 键时显示为下划线。您可以通过设置 Label 控件的目标属性来指定目标控件。以下示例演示了页面间隔文本框的助记键设置。按下 Alt+P 时,焦点会转移到页面间隔文本框。
<Label VerticalAlignment="Center"
Margin="10,0,0,0"
Target="{Binding ElementName=txtInterval}"
Content="_Pages"></Label>
如前所述,应用程序中使用的所有助记键在运行时按下 Alt 键时都会显示下划线,因此用户可以轻松识别和使用这些键。除了助记键之外,按钮控件还公开了一些使应用程序对键盘更友好的属性,即 IsDefault
属性和 IsCancel
属性。IsDefault
属性确定特定按钮是否被视为视图的默认按钮。当 IsDefault
设置为 true
时,当用户在视图中按 Enter 键时,将引发按钮的单击事件。类似地,IsCancel
属性确定该按钮是否应被视为取消按钮。当 IsCancel
设置为 true
时,当按 Esc 键时,将引发按钮的单击事件。
<Button ToolTip="Start PDF split process"
VerticalAlignment="Center"
Command="{Binding SplitPdfCmd}"
Margin="0,0,2,0"
IsDefault="True"/>
<Button ToolTip="Cancel PDF split process"
Margin="2,0,5,0"
VerticalAlignment="Center"
Command="{Binding CancelSplitPdfCmd}"
IsCancel="True"/>
按钮控件支持访问键,这与标签支持的助记键类似。当按钮内容中的字母前面加上下划线 (_) 符号时,按下 Alt 键时该字母会显示下划线,当用户同时按下 Alt 键和该键时,按钮将被单击。例如,覆盖复选框(继承自 buttonbase
)定义如下
<CheckBox ToolTip="Overwrite resulting PDF files if they exist in location"
IsChecked="{Binding OverwriteFile,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Margin="0,5,0,0"
Content="_Overwrite file(s) if exists"></CheckBox>
覆盖模式显示为“如果文件存在则覆盖文件”,其中第一个字母“O”在按下 Alt 键时会显示下划线,按下 Alt+O 时会切换复选框。
可以通过默认手势 Ctrl+Tab 或 Ctrl+Shift+Tab 来切换不同的标签页。
参考文献
- MCTS 自学培训套件考试 70-511
关注点
如果您对该工具或本文有任何改进建议,请告诉我。
请注意,用于绑定的继承属性必须是 public
的,protected
属性不能用于绑定。
webbrowser
控件使用 acrobat PDF 阅读器插件来显示 PDF 文件。如果用户的计算机上安装了 Adobe Reader,它也会安装 acrobat PDF 阅读器插件。我在 WebBrowser
控件中显示 PDF 文件时遇到的一个问题是,即使在关闭 WebBrowser
控件后,它们仍然被锁定。我在 这篇文章 中找到了解决此问题的方法。
//Avoid PDF lock by AcroRd32.dll after closure
private void OnCleanUpCmdExecute(object parameter)
{
WebBrowser webBrowser = (WebBrowser)parameter;
if (webBrowser != null)
{
App.Current.Dispatcher.BeginInvoke(new Action(delegate ()
{
webBrowser.NavigateToString("about:blank");
}));
}
}
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<i:InvokeCommandAction Command="{Binding CleanUpCmd}"
CommandParameter="{Binding ElementName=mainWebBrowser}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
可以通过在应用程序中嵌入 PDF 查看器来改进此应用程序,这样应用程序就不依赖于本地安装的 PDF 插件。
历史
- 2015年10月31日:版本 1.0.0.0 - 创建文章
- 2015年12月19日:版本 1.0.0.0 - 添加了代码和安装程序包的链接