65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WPF 应用中使用 Adobe Reader

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (12投票s)

2012 年 5 月 13 日

CPOL

9分钟阅读

viewsIcon

131118

downloadIcon

8794

一篇关于如何将 Adobe Reader COM 对象适配为与 MVVM 良好兼容的文章

简介

本文介绍如何使用 Adobe PDF Reader COM 组件在 WPF 应用程序中使用,并且无需代码隐藏(感谢 Expression Blend Behaviors 的一点帮助)。尽管本文使用了 Expression Blend,但它并非必需,因为通过下载免费的 Expression Blend SDK 并手动输入所需的 XAML,可以达到相同的效果(这只需要更多的努力——导入正确的命名空间等)。

背景

我们将创建的应用程序非常简单——它允许用户在 WPF 窗口中查看 PDF、调用打印操作以及更改当前显示的 PDF。这足以演示本文中的概念。

假定您已经了解使用 MVVM 的好处;理解基本细节,例如INotifyPropertyChangedICommand 的用法,以及避免在代码隐藏中编写逻辑的优势(尽管有时代码隐藏是合适的)。如果您不了解 MVVM 的基础知识,我建议您先查看外部链接部分的一些链接。

本文采用教程的形式——假定您正在使用 Visual Studio 2010 和 Expression Blend 4(或已安装 Expression Blend 4 SDK)以及 Adobe PDF Reader。对于没有 Visual Studio 付费许可证的用户,Visual C# 2010 Express 应该足够了。

步骤 1 在 Visual Studio 中创建新的 WPF 应用程序

第一步是在 Visual Studio 中创建一个新的 WPF 应用程序。完成后,您需要创建一个 Windows Forms 用户控件来托管 Adobe PDF Reader。

您可能会问为什么有必要这样做?WPF 不能直接使用 ActiveX 控件。要使 ActiveX 控件在 WPF 中可用,它必须托管在 Windows Forms 控件中。要在 WPF 中使用 Windows Forms 控件,它们本身必须托管在WindowsFormsHost 元素中。这种关系如下图所示

要创建 Windows Forms 用户控件,请从项目的上下文菜单中选择“添加新项”,在“搜索已安装的模板”文本框中输入“用户控件”,然后将文件命名为 PdfViewer.cs

您也可以将此控件放在 Windows Forms 控件库中,但为保持简单,我们将不这样做。

将此控件添加到项目后,Windows Forms 设计器应会打开一个空白画布。您需要打开工具箱(CTRL + W, X)。第一步,最好为自定义控件添加一个新选项卡——这是工具箱上下文菜单中的一个选项。展开此新选项卡后,从上下文菜单中选择“选择项”。当“选择工具箱项”对话框出现时,选择“COM 组件”选项卡并选择“Adobe PDF Reader”(这将把 AcroPDF.DLL 添加到工具箱)。

按 OK——您现在应该在工具箱中看到 Adobe PDF Reader。只需双击它即可将组件添加到用户控件中。


选择组件的属性窗口(F4),将其名称更改为更有意义的名称(我称之为 acrobatViewer),并将 Dock 属性更改为 Fill。


现在,我们已成功将 Adobe ActiveX 控件添加到我们的 Windows Forms UserControl 中。下一步是为此控件添加一个自定义属性和方法,以便可以使用它。打开此控件的代码隐藏(从设计器按 F7)并添加粗体所示的代码

public partial class PdfViewer : UserControl
{
 private string pdfFilePath;

 public PdfViewer()
 {
  InitializeComponent();
  acrobatViewer.setShowToolbar(false);
  acrobatViewer.setView("FitH");
 }

 public string PdfFilePath
 {
  get
  {
   return pdfFilePath;
  }

  set
  {
   if (pdfFilePath != value)
   {
    pdfFilePath = value;
    ChangeCurrentDisplayedPdf();
   }
  }
 }

 public void Print()
 {
  acrobatViewer.printWithDialog();
 }

 private void ChangeCurrentDisplayedPdf()
 {
  acrobatViewer.LoadFile(PdfFilePath);
  acrobatViewer.src = PdfFilePath;
  acrobatViewer.setViewScroll("FitH", 0);
 }
}

在构造函数中,我们告诉控件隐藏工具栏并将阅读器设置为使用 FitH(水平适应)。在类的其余部分,我们添加了一个“PdfFilePath”属性,以便可以更改当前显示的 PDF,以及一个允许打印 PDF 的方法,该方法显示打印机选项对话框。私有方法 ChangeCurrentDisplayedPdf 仅更改查看器中当前显示的 PDF 并将滚动位置设置为文档的顶部。

此时,我们的 Windows Forms 组件已具有足够的功能,可以从 WPF 使用。下一步是继承 WindowsFormsHost 来包装我们的自定义 Windows Forms 组件,并添加一个 DependencyProperty,以便我们的自定义 WPF 元素易于与 WPF 数据绑定一起使用。

首先,将 WindowsFormsIntegration 程序集添加到项目中。创建一个名为 PdfViewerHost 的新类,并添加以下代码

public class PdfViewerHost : WindowsFormsHost
{
 public static readonly DependencyProperty PdfPathProperty = DependencyProperty.Register(
  "PdfPath", typeof(string), typeof(PdfViewerHost), new PropertyMetadata(PdfPathPropertyChanged));
 
 private readonly PdfViewer wrappedControl;

 public PdfViewerHost()
 {
  wrappedControl = new PdfViewer();
  Child = wrappedControl;
 }

 public string PdfPath
 {
  get
  {
   return (string)GetValue(PdfPathProperty);
  }

  set
  {
   SetValue(PdfPathProperty, value);
  }
 }
   
 public void Print()
 {
  wrappedControl.Print();
 }

 private static void PdfPathPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
 {
  PdfViewerHost host = (PdfViewerHost)d;
  host.wrappedControl.PdfFilePath = (string)e.NewValue;
 }
}

现在我们有了一个包装 Windows Forms 用户控件的类,该类又包装了 Adobe Reader COM 组件

此控件的主要部分是一个依赖属性,它包装 Windows Forms 控件的 PdfFilePath 属性。这是一个标准的 DependencyProperty,它使用 PropertyChangedCallback 委托在每次依赖属性的值更改时更改被包装的对象属性。值得注意的是,当在运行时使用 XAML 时,SetValue 方法会直接调用,因此 .NET 属性包装器中没有用于更改 Windows Forms 用户控件属性的代码。该控件还有一个 Print 方法,它只是委托给底层的 Windows Forms 控件。


现在我们有了使 ActiveX 控件能很好地与 WPF 及其绑定系统配合使用的类,我们需要创建一个简单的 ViewModel。在实际应用程序中,我建议至少为您的 ViewModels 创建一个基类,或者最好使用流行的框架,如 MVVM Light 或 Microsoft PRISM。但出于本文的需要,我们将创建一个虚构的 ViewModel,它只实现 INotifyPropertyChanged。

在创建 ViewModel 之前,您需要将 Microsoft.Expression.Interactions.dll 程序集(可通过 Expression Blend SDK 获得)添加到引用中。此库包含ActionCommand 类,它是一个 ICommand 实现——类似于 Josh Smith 的 RelayCommand 或 PRISM 的 DelegateCommand。通常我不会使用它,而是倾向于使用 RelayCommand(因为它更常用并且支持泛型),但是由于我们将引用 Expression Blend SDK 程序集,我认为我们也可以在我们的应用程序中使用它。

我们的 ViewModel 类及其所有功能如下

 
public class MainWindowViewModel : INotifyPropertyChanged
{
 private readonly string[] pdfChoices;
 private string currentPdf;
 private int currentPdfChoiceIndex;

 public MainWindowViewModel()
 {
  if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
  {
   return;
  }

  string currentFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
  pdfChoices = new string[]
        {
         Path.Combine(currentFolder, "SamplePDF1.pdf"),
         Path.Combine(currentFolder, "SamplePDF2.pdf"),
        };

  CurrentPdf = pdfChoices[currentPdfChoiceIndex];

  SwapPdfsCommand = new ActionCommand(() =>
  {
   currentPdfChoiceIndex ^= 1;
   CurrentPdf = pdfChoices[currentPdfChoiceIndex];
  });
 }

 public event PropertyChangedEventHandler PropertyChanged;

 public string CurrentPdf
 {
  get
  {
   return currentPdf;
  }

  set
  {
   if (currentPdf != value)
   {
    currentPdf = value;
    OnPropertyChanged(new PropertyChangedEventArgs("CurrentPdf"));
   }
  }
 }

 public ICommand SwapPdfsCommand
 {
  get;
  private set;
 }

 protected void OnPropertyChanged(PropertyChangedEventArgs e)
 {
  PropertyChangedEventHandler handler = PropertyChanged;

  if (handler != null)
  {
   handler(this, e);
  }
 }
}

在类图上,更容易看出 View 只有两件事是相关的——CurrentPdf 和 SwapPdfsCommand 属性。

DesignerProperties.GetIsInDesignMode 代码只是一个技巧,用于允许我们在 Blend 中进行连接,并防止 Visual Studio/Blend 设计器崩溃。在生产代码中,您不希望在 ViewModel 中出现此代码。有关正确技术,请在您喜欢的搜索引擎中搜索“ViewModelLocator”、“Blendability”和“Design Time Data”(我可能会在本文的未来版本中正确实现它)。值得记住的是,在 XAML 中实例化的任何 ViewModels,在 Visual Studio 和 Expression Blend 的设计模式下都会调用它们的构造函数。

在构造函数中,代码填充了一个数组,其中包含与可执行文件位于同一目录的两个 PDF 文档的路径(代码下载中包含这些)。此代码仅用于演示目的——在实际世界中,您可能会有一个服务(使用依赖注入框架)注入到 ViewModel 中来创建这些。

我们连接了一个 ActionCommand,它在数组中的两个值之间切换字符串属性 CurrentPdf(使用简单的 XOR 逻辑来实现)。该类还包含一个包含 Command 的属性(允许它绑定到 View),以及一个包含 CurrentPdf 的字符串属性。

至此,我们已经编写了所有必要的 C# 代码,以使 Adobe Reader 控件在 WPF 中能够很好地工作。最后一步是向项目中添加两个 PDF 文件,分别命名为 SamplePDF1.pdf 和 SamplePDF2.pdf,并将“复制到输出目录”设置为“如果较新则复制”。

另外,由于目前没有 Acrobat DLL 的 x64 版本,因此最好确保目标平台设置为 x86(这可以在项目属性的生成选项卡中找到),否则会发生类似“类未注册(异常来自 HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG))”的错误——在我看来相当含糊不清!

为确保此时一切正常,您应该在 Visual Studio 中生成解决方案。

步骤 2 在 Expression Blend 中连接所有内容

在 Expression Blend 中打开解决方案文件,然后在“对象和时间线”面板中选择“窗口”对象。然后在属性面板中选择“通用属性”下的 DataContext 属性(如果使用搜索功能会更容易)

在 DataContext 属性的右侧,选择“新建”,然后选择 MainWindowViewModel 类。这会简单地调整我们的 XAML,以便设置窗口的 Data Context
<Window.DataContext>
 <local:MainWindowViewModel/>
</Window.DataContext>

接下来,添加以下 XAML(替换默认的 Grid)来定义我们页面的外观(随意使用设计器或直接输入 XAML)

<DockPanel>
 <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="3">
  <StackPanel.Resources>
   <Style TargetType="Button">
    <Setter Property="Margin" Value="3" />
   </Style>
  </StackPanel.Resources>
  <Button Content="Print PDF" />
  <Button Content="Change PDF File"/>
 </StackPanel>
 <local:PdfViewerHost x:Name="PdfViewer" />
</DockPanel>

此 XAML 定义了我们窗口的布局——一个 DockPanel,其中填充了我们的自定义 PdfViewHost,以及一个带有两个按钮的 StackPanel——一个用于打印,另一个用于更改 PDF——非常简单。在“对象和时间线”面板中,选择我们的 PDFViewerHost,然后找到 PDFPath 属性(位于 Miscellaneous 下)。选择高级选项、数据绑定,然后选择 CurrentPdf

现在运行项目,您将在查看器中看到显示的 PDF,但两个按钮都不会执行任何操作。要更改此设置,请在“对象和时间线”面板中选择“更改 PDF 文件”按钮,然后选择属性 --> Miscellaneous --> command --> advanced options --> Data binding,然后选择 SwapPdfsCommand。

最后,在 Assets 面板中,在 Behaviors 部分选择 CallMethodAction,并将其拖到“打印 PDF”按钮上。选择此操作的属性面板。在“通用属性”下选择“目标对象”-->“数据绑定”-->“元素属性”-->“PdfViewer”-->“OK”。在方法名中键入 Print。

最终生成的 Window 元素内的 XAML 应该如下所示

  <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <DockPanel>
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="3">
            <StackPanel.Resources>
                <Style TargetType="Button">
                    <Setter Property="Margin" Value="3" />
                </Style>
            </StackPanel.Resources>
            <Button Content="Print PDF">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <behaviours:CallMethodAction TargetObject="{Binding ElementName=PdfViewer}" MethodName="Print" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
            <Button Command="{Binding SwapPdfsCommand}" Content="Change PDF File"/>
        </StackPanel>
        <local:PdfViewerHost x:Name="PdfViewer" PdfPath="{Binding CurrentPdf}" />
    </DockPanel>

在后台,Expression Blend 已添加了对 System.Windows.Interactivity.dll 的引用,但您也可以通过在 Visual Studio 中引用相同的 DLL 并手动输入代码来轻松实现相同的结果。那样会慢一些,但如果您没有 Expression Blend,那也是一个不错的选择。通过几个步骤,我们成功地将一个旧的 COM 组件完全集成到 WPF 强大的数据绑定机制中。

使用代码

我没有用 Express 版的 Visual Studio 测试过代码,但看不出它为什么不能工作。

致谢

我想感谢 Jason Hunt 审阅本文。

外部链接

Microsoft Expression - 使用内置行为

MSDN 演练 - 在 WPF 中托管 ActiveX 控件

MSDN 文档 - WindowsFormsHost

C# 在线 WPF 概念 - 依赖属性实现

Microsoft 下载 - Expression Blend SDK for .NET 4

MSDN 杂志 - 使用 Model-View-ViewModel 设计模式的 WPF 应用

In the Box - MVVM 培训

MVVM 框架 - MVVM Light

Microsoft Patterns and Practises - PRISM 框架

历史 

2012-05-12 - 在 code project 上发布了文章。

© . All rights reserved.