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

MoonPdfPanel - 一个基于WPF的PDF查看器控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (33投票s)

2013年4月18日

GPL3

17分钟阅读

viewsIcon

208128

downloadIcon

8605

本文介绍了MoonPdfPanel的工作原理以及如何将其集成到您的应用程序中

MoonPdf Sample Image

目录

引言

wmjordan 类似,他撰写了CodeProject文章 使用Mupdf和P/Invoke在C#中渲染PDF文档,我一直在寻找一个免费的原生.NET PDF渲染引擎。和 [wmjordan] 一样,我没有找到任何,所以使用了他的解决方案,该方案使用MuPdf将PDF页面渲染为图像。

基于他的代码,我编写了WPF用户控件 MoonPdfPanel,它可以轻松地用于在基于.NET的应用程序中显示PDF文件。为了演示 MoonPdfPanel 的使用,我编写了一个名为 MoonPdf 的示例WPF应用程序。MoonPdf 可以被认为是一个非常基础的PDF查看器/阅读器。它使用了 MoonPdfLib 程序集,其中包含上述提到的 MoonPdfPanel。上面的截图显示了加载了示例PDF文件的 MoonPdf 应用程序。

在本文中,我将展示 MoonPdfPanel 的工作原理以及如何将其集成到您的应用程序中以显示PDF文件。

相关且有用的项目

有两篇CodeProject文章在 MoonPdf 的创建过程中给了我很大的帮助

如上所述,第一篇文章帮助我了解了如何使用MuPdf将PDF页面渲染为图像。第二篇文章为WPF中的数据虚拟化提供了一个有用的解决方案。这段代码被用来实现一个虚拟化面板,从而能够实现PDF页面的连续布局。它允许我虚拟化PDF页面,即不需要一次性加载所有页面。这提高了性能并降低了内存消耗。我将在后面详细解释这些实现的细节。

构建和包含MuPdf渲染引擎

为了渲染PDF页面,我使用了MuPdf渲染引擎。MuPdf也用于著名的PDF阅读器 SumatraPDF。SumatraPDF 的开发者们已经做了出色的工作,提供了 nmake 文件,用于从MuPdf源代码构建一个DLL。所以最终,我包含了SumatraPDF的源代码来构建一个MuPdf DLL (libmupdf.dll)。这个DLL对于使用 在C#中使用Mupdf和P/Invoke渲染PDF文档 中提出的解决方案是必需的。

为了将DLL包含到构建过程中,我编写了一个小的 nmake 文件,用于构建和复制 libmupdf.dll。以下源代码显示了用于编译整个源代码的 msbuild 文件。在构建MoonPdf解决方案之前,使用我的 nmake 文件 makefile-mupdf.msvc (此处未显示) 构建MuPdf源代码。之后,将编译好的 libmupdf.dll (下面源代码中加粗部分) 相应地复制。

<Project DefaultTargets="Build"
	xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <platform Condition="$(platform) == ''">X86</platform> 
    </PropertyGroup>
    <Target Name="Build">
        <RemoveDir Directories="ext/sumatra/output/$(platform)" />
        <exec Command="nmake -f makefile-mupdf.msvc platform=$(platform)"/>
        <Copy SourceFiles="ext/sumatra/output/$(platform)/libmupdf.dll"
	 DestinationFolder="bin/MuLib/$(platform)" />
        <MSBuild Projects="src/MoonPdf.sln" Targets="Rebuild"
	 Properties="Configuration=Release;Platform=$(platform);AllowUnsafeBlocks=true"/>
    </Target>
</Project>

渲染PDF页面

PDF页面的渲染非常直接。这一切都发生在 MuPdfWrapper 类中。主要部分在 ExtractPage 方法中完成,该方法如下所示。该方法期望一个 IPdfSource 对象 (参见下面的代码)、要渲染的页码以及渲染时应用的缩放因子。该方法返回一个 Bitmap 对象,该对象稍后会被转换为 BitmapSource 对象,以便在WPF中使用 (参见 下一节)。

public static Bitmap ExtractPage(IPdfSource source, int pageNumber,
                                 float zoomFactor = 1.0f)
{
    var pageNumberIndex = Math.Max(0, pageNumber - 1); // pages start at index 0

    using (var stream = new PdfFileStream(source))
    {
        IntPtr p = NativeMethods.LoadPage(stream.Document, pageNumberIndex);
        var bmp = RenderPage(stream.Context, stream.Document, p, zoomFactor);
        NativeMethods.FreePage(stream.Document, p);

        return bmp;
    }
}

MoonPdf允许从文件或内存加载PDF文档。上面提到的接口 IPdfSource 是两个源 (FileSourceMemorySource) 的公共接口。

大部分非托管资源被封装在 PdfFileStream 类中,该类实现了 IDisposable 接口。PdfFileStream 对象的使用在上面的 using 语句中所示。对于渲染,ExtractPage 方法利用了 RenderPage 方法。这个方法 (以及其余的互操作代码) 可以在 此处 查看。我只对 RenderPage 方法进行了轻微修改,以考虑缩放因子。修改如下所示。为清晰起见,我省略了其余 (不短) 的方法。

static Bitmap RenderPage(IntPtr context, IntPtr document, IntPtr page, float zoomFactor)
{
    ...
    Rectangle pageBound = NativeMethods.BoundPage(document, page);

    // gets the size of the page and multiplies it with zoom factors
    int width = (int)(pageBound.Width * zoomFactor);
    int height = (int)(pageBound.Height * zoomFactor);

    // sets the matrix as a scaling matrix (zoomX,0,0,zoomY,0,0)
    Matrix ctm = new Matrix();
    ctm.A = zoomFactor;
    ctm.D = zoomFactor;
    
    ...
}

好吧,关于PDF页面的渲染就差不多了。稍后,我将展示 ExtractPage 方法在哪里被调用,以显示渲染的图像。

显示渲染的PDF页面

基础知识

在进一步解释细节之前,我需要澄清一些我将要使用的术语

  • PDF页面:PDF文档中的一页,被渲染成位图。
  • 页面行:包含一到两页PDF页面 (两页PDF页面并排显示)。

在MoonPdf中,页面行的视图类型由 ViewType 枚举来处理

public enum ViewType
{
    SinglePage,
    Facing,
    BookView
}

ViewType.SinglePage 是最简单的情况,其中PDF页面和页面行是相同的,这意味着一个页面行只包含一个PDF页面。ViewType.Facing 表示每个页面行包含两个PDF页面 (除非最后只剩一个PDF页面可放入页面行)。ViewType.BookViewViewType.Facing 相同,只是它以第一个页面行中的单个PDF页面开始。

为了说明视图类型,我们来看下面的图。它显示了MoonPdf中带有 ViewType.Facing 视图类型的示例PDF。因此,图显示了一个包含两个PDF页面的页面行。

除了视图类型之外,第二个重要的布局方面是页面行的显示方式。这种行为由 PageRowDisplayType 枚举处理 (参见下面的代码)。PageRowDisplayType.SinglePageRow 值用于一次只显示一个页面行。这在上面的图中有显示。另一个选项是 PageRowDisplayType.ContinuousPageRows,它连续显示页面行。这种显示类型的示例显示在 第一个示例图 中。

public enum PageRowDisplayType
{
    SinglePageRow = 0,
    ContinuousPageRows
}

实现 (用户控件)

这两种页面行类型的布局逻辑差异很大,因此我决定为每种类型实现一个用户控件。我创建了两个用户控件 SinglePageMoonPdfPanel.xamlContinuousMoonPdfPanel.xaml。尽管它们的行为不同,但它们有一个共同点,即页面行总是包含一到两个PDF页面并排显示。实现这一点的最简单方法是使用 ItemsControl 并将其 ItemsPanel 定义为具有水平方向的 StackPanelItemsControl 的项是 Image 对象,它们将包含渲染的PDF页面作为图像。我将两个用户控件通用的XAML封装在一个全局 ResourceDictionary 中,名为 GlobalResources.xaml,以便两个用户控件都可以使用它。该XAML如下所示。XAML还包含图像源和边距的数据绑定。我将在稍后解释它们的用途。

<ResourceDictionary ...>
    <Style x:Key="moonPdfItems" TargetType="{x:Type ItemsControl}">
        <Setter Property="ItemTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Image Source="{Binding ImageSource}" Margin="{Binding Margin}"
                           HorizontalAlignment="Center"
                           UseLayoutRounding="True" Stretch="None"
                           RenderOptions.BitmapScalingMode="NearestNeighbor" />
                </DataTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

SinglePageMoonPdfPanel 的布局非常直接,因为它一次只包含一个页面行。因此,我们只需要一个 ItemsControl 来管理这一个页面行中的PDF页面。ItemsControl 及其使用的样式 (参见上面的XAML) 在下面的XAML中加粗显示。ControlTemplate 使用一个 ScrollViewer 元素,允许内容滚动。在 ScrollViewer 上,我将 FocusVisualStyle 设置为 {x:Null},以移除控件获得焦点时通常显示的虚线矩形。

<UserControl x:Class="MoonPdfLib.SinglePageMoonPdfPanel">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="GlobalResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <ItemsControl x:Name="itemsControl" ItemsSource="{Binding}"
                  Style="{StaticResource moonPdfItems}">
        <ItemsControl.Template>
            <ControlTemplate TargetType="{x:Type ItemsControl}">
                <ScrollViewer FocusVisualStyle="{x:Null}">
                    <ItemsPresenter />
                </ScrollViewer>
            </ControlTemplate>
        </ItemsControl.Template>
    </ItemsControl>
</UserControl>

与上面的 SinglePageMoonPdfPanel 相比,ContinuousMoonPdfPanel 包含多个页面行,因此这里需要一个额外的 ItemsControl 来管理多个页面行。下面的代码显示了 ContinuousMoonPdfPanel 的XAML。我已经将两个使用的 ItemsControl 加粗显示。第一个负责页面行。第二个负责并排显示图像。它使用了 GlobalResources.xaml 中键为 moonPdfItems 的样式。

<UserControl x:Class="MoonPdfLib.ContinuousMoonPdfPanel"
             xmlns:virt="clr-namespace:MoonPdfLib.Virtualizing" ...>
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="GlobalResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <ItemsControl Name="itemsControl">
        <ItemsControl.Template>
            <ControlTemplate TargetType="{x:Type ItemsControl}">
                <ScrollViewer CanContentScroll="True" FocusVisualStyle="{x:Null}">
                    <ItemsPresenter />
                </ScrollViewer>
            </ControlTemplate>
        </ItemsControl.Template>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <ItemsControl ItemsSource="{Binding}" 
                Style="{StaticResource moonPdfItems}">
                    <ItemsControl.Template>
                        <ControlTemplate TargetType="{x:Type ItemsControl}">
                            <ItemsPresenter />
                        </ControlTemplate>
                    </ItemsControl.Template>
                </ItemsControl>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <virt:CustomVirtualizingPanel />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</UserControl>

上述代码中一个有趣的部分是使用了 CustomVirtualizingPanel (加粗部分)。该面板继承自 System.Windows.Controls.VirtualizingPanel,允许我们虚拟化页面行。这意味着我们只需要将当前页面行加载到内存中。当用户在文档中进一步滚动时,这也允许我们处理掉之前的页面行。这个虚拟化面板非常重要,因为在“普通”的项面板中,我们必须在显示之前加载和添加所有PDF页面。这将大大增加内存消耗,并且对于较大的PDF文件,我们的应用程序将无法使用。但是通过虚拟化,内存消耗保持在可控范围内。稍后将讨论虚拟化,并继续解释更多关于控件的细节。

由于我为单页和连续布局创建了两个单独的控件,因此我需要一种方法将它们包装在一个易于集成的用户控件中。所以我创建了一个“包装器”面板,名为 MoonPdfPanel,它包含适当的用户控件 (SinglePageMoonPdfPanelContinuousMoonPdfPanel),具体取决于所选的 PageRowDisplayType。为了实现这一点,上述两个用户控件需要一个共同的基础或接口,所以我决定创建一个由这两个用户控件实现的接口 IMoonPdfPanel。该接口如下所示。

internal interface IMoonPdfPanel
{
    ScrollViewer ScrollViewer { get; }
    UserControl Instance { get; }
    float CurrentZoom { get; }
    void Load(IPdfSource source, string password = null);
    void Zoom(double zoomFactor);
    void ZoomIn();
    void ZoomOut();
    void ZoomToWidth();
    void ZoomToHeight();
    void GotoPage(int pageNumber);
    void GotoPreviousPage();
    void GotoNextPage();
    int GetCurrentPageIndex(ViewType viewType);
}

接口由以下类实现

internal partial class SinglePageMoonPdfPanel : UserControl, IMoonPdfPanel
{...}

internal partial class ContinuousMoonPdfPanel : UserControl, IMoonPdfPanel
{...}

包装器面板 MoonPdfPanel 只有一个对通用接口 IMoonPdfPanel 的引用。MoonPdfPanel 将操作,例如缩放或导航,委托给 IMoonPdfPanel 的当前实例。下面的代码示例展示了一个例子。

public partial class MoonPdfPanel : UserControl
{
    ...
    private IMoonPdfPanel innerPanel;
    ...

    public void GotoNextPage()
    {
        this.innerPanel.GotoNextPage();
    }
}

包装器面板 MoonPdfPanel 的XAML如下所示

<UserControl x:Class="MoonPdfLib.MoonPdfPanel" ...>
    <DockPanel LastChildFill="True" x:Name="pnlMain">
    </DockPanel>
</UserControl>

正如你所见,XAML非常简单。该用户控件只包含一个 DockPanel,其中将添加适当的用户控件 (SinglePageMoonPdfPanelContinuousMoonPdfPanel)。这在 MoonPdfPanel 的代码后台中显示。每当 PageRowDisplayType 更改时,当前的 innerPanel 会从停靠面板 (pnlMain) 中移除。然后创建一个新的实例 (取决于 PageRowDisplayType) 并将其添加到 dockpanel 中。

// we need to remove the current innerPanel
this.pnlMain.Children.Clear();

if (pageRowDisplayType == PageRowDisplayType.SinglePageRow)
    this.innerPanel = new SinglePageMoonPdfPanel(this);
else
    this.innerPanel = new ContinuousMoonPdfPanel(this);

this.pnlMain.Children.Add(this.innerPanel.Instance);

实现 (数据绑定和虚拟化)

前面的XAML 所示,PDF页面的数据绑定有两个属性 ImageSourceMargin。这些是 PdfImage 类的一部分 (参见下方)。ImageSource 属性用于保存PDF页面的图像,Margin 属性用于定义PDF页面的边距,即页面之间的水平边距 (使用 ViewType.FacingViewType.BookView 时)。对于 Margin,只使用了 Thickness 结构的 Right 属性,但我选择 Thickness 结构而不是简单的 double,因为它使数据绑定更容易。

internal class PdfImage
{
    public ImageSource ImageSource { get; set; }
    public Thickness Margin { get; set; }
}

加载所需PDF页面的逻辑封装在 PdfImageProvider 类中。该类实现了 Paul 的数据虚拟化解决方案 中的泛型 IItemsProvider 接口。PdfImageProvider 的两个重要方法是 FetchCountFetchRange (参见下方)。

internal class PdfImageProvider : IItemsProvider<IEnumerable<PdfImage>>
{
    ...
    public int FetchCount()
    {
        if (count == -1)
            count = MuPdfWrapper.CountPages(pdfSource);

        return count;
    }

    public IList<IEnumerable<PdfImage>> FetchRange(int startIndex, int count)
    {
        for(...)
        {
            using (var bmp = MuPdfWrapper.ExtractPage(pdfSource, i, this.Settings.ZoomFactor))
            {
                ...
                var bms = bmp.ToBitmapSource();
                // Freeze bitmap to avoid threading problems when using AsyncVirtualizingCollection,
                // because FetchRange is NOT called from the UI thread
                bms.Freeze(); 
                var img = new PdfImage { ImageSource = bms, Margin = margin };
                ...
            }
        }
    }
    ...
}

第一个方法与 ContinuousMoonPdfPanel 中使用的数据虚拟化相关。它返回虚拟化项的数量。在我们的例子中,这是PDF文档中的页数。借助 MuPdfWrapper,可以轻松确定这个数字,它提供了 CountPages 方法。

另一个方法 FetchRange 用于检索要显示的 PdfImage。上面的代码显示了调用 ExtractPage 方法来获取相应PDF页面的位图。然后,借助自定义扩展方法 ToBitmapSource (此处未显示),将此位图转换为 BitmapSource 对象。在这个 BitmapSource 对象上,我们调用 Freeze 方法,使其不可修改。这一点很重要,因为对于 ContinuousMoonPdfPanel,我们使用了 Paul 的 AsyncVirtualizingCollection (参见 此处),它会在另一个线程上异步调用 FetchRange。由于 BitmapSource 对象不是在UI线程上创建的,如果调用的不是 Freeze 方法,那么稍后的绑定 (发生在UI线程上) 将会失败。

在上述步骤之后,会创建一个新的 PdfImage 对象并用相应的值填充。FetchRange 方法返回一个页面行列表,其中一个页面行表示为 IEnumerable<PdfImage> 对象。这个列表稍后用于数据绑定 (参见下方)。

FetchRange 方法从 SinglePageMoonPdfPanel 调用时,只需要列表中的第一个项 (第一个页面行),因为该面板一次只显示一个页面行。这看起来像这样

this.itemsControl.ItemsSource = this.imageProvider.FetchRange
(startIndex, this.parent.GetPagesPerRow()).FirstOrDefault();

FirstOrDefault 的方法调用中,你可以看到我们只关心结果列表中的第一个项,它是一个 IEnumerable<PdfImage> 类型的对象。

当我们使用 ContinuousMoonPdfPanel 时,我们不会显式调用 FetchRange 方法。相反,我们利用了 Paul 文章中的泛型 AsyncVirtualizingCollection。这个类负责虚拟化管理。它期望一个 IItemsProvider 对象,我们通过一个 PdfImageProvider 对象来提供。FetchRange 方法 (它是 IItemsProvider 接口的一部分) 将由 AsyncVirtualizingCollection 对象隐式调用,每当请求新项时 (例如,当用户滚动文档时)。以下行显示了在 ContinuousMoonPdfPanel 中如何分配项源。

this.itemsControl.ItemsSource = new AsyncVirtualizingCollection<IEnumerable<PdfImage>>
(this.imageProvider, this.parent.GetPagesPerRow(), pageTimeout);

要使虚拟化生效的一个重要部分是上面 提到的 CustomVirtualizingPanel。这里是上面XAML相关部分的一个摘录。第一个加粗文本显示了引用clr命名空间的xml命名空间,以包含用户控件。第二个加粗文本显示了 CustomVirtualizingPanel 被用作我们 ItemsControlItemsPanel

<UserControl x:Class="MoonPdfLib.ContinuousMoonPdfPanel"
             xmlns:virt="clr-namespace:MoonPdfLib.Virtualizing" ...>
    ...
    <ItemsControl Name="itemsControl">
        ...
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <virt:CustomVirtualizingPanel />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</UserControl>

由于项是虚拟化的,CustomVirtualizingPanel 需要知道项所需的空间量,即所有虚拟化项的最大宽度和总高度。这对于滚动条的正确行为至关重要。计算所需空间的第一个步骤是获取给定文档的所有PDF页面的边界。这通过 MuPdfWrapper 来完成,该函数使用本地 BoundPage 方法,该方法为给定的PDF页面提供一个 Rectangle。下面的方法获取所有页面边界作为 Size[]。它还会考虑页面可能的旋转。如果未指定旋转或指定了180度旋转,我们无需更改任何内容。但否则,我们切换边界的宽度和高度 (参见加粗文本)。这可以通过委托 sizeCallback 在这里实现。

public static System.Windows.Size[] GetPageBounds(IPdfSource source,
                                       ImageRotation rotation = ImageRotation.None)
{
    Func<double, double, System.Windows.Size> sizeCallback =
                        (width, height) => new System.Windows.Size(width, height);
    
    if( rotation == ImageRotation.Rotate90 || rotation == ImageRotation.Rotate270 )
    {
        // switch width and height
        sizeCallback = (width, height) => 
        new System.Windows.Size(height, width);
    }

    using (var stream = new PdfFileStream(source))
    {
        var pageCount = NativeMethods.CountPages(stream.Document);
        var resultBounds = new System.Windows.Size[pageCount];

        for (int i = 0; i < pageCount; i++)
        {
            IntPtr p = NativeMethods.LoadPage(stream.Document, i); // loads the page
            Rectangle pageBound = NativeMethods.BoundPage(stream.Document, p);

            resultBounds[i] = sizeCallback(pageBound.Width, pageBound.Height);

            NativeMethods.FreePage(stream.Document, p); // releases the resources consumed by the page
        }

        return resultBounds;
    }
}

但是知道PDF页面的边界只是故事的一部分,因为我们需要考虑到,一个页面行中可能显示两个PDF页面,这将导致不同的所需空间。因此,在知道单个PDF页面的边界后,我们计算所有页面行的所需空间。这在 CalculatePageRowBounds 方法中完成 (参见下方)。页面行的所需宽度是相关PDF页面宽度之和加上它们之间的选定水平边距。页面行的所需高度是相关PDF页面高度的最大值加上选定的垂直边距。

示例:假设选择了 ViewType.Facing (页面行中有两个PDF页面),并且两个PDF页面的大小均为 600x800 像素 (宽度 x 高度),水平和垂直边距均为 4 像素。那么页面行的计算大小将是 1204x1604 像素。必须为所有页面行进行此计算,因为某些PDF页面的宽度或高度可能比其他页面宽或高。

private PageRowBound[] CalculatePageRowBounds(Size[] singlePageBounds, ViewType viewType)
{
    var pagesPerRow = Math.Min(GetPagesPerRow(), singlePageBounds.Length);
    var finalBounds = new List<PageRowBound>();
    var verticalBorderOffset = (this.PageMargin.Top + this.PageMargin.Bottom);

    if (viewType == MoonPdfLib.ViewType.SinglePage)
    {
        finalBounds.AddRange(singlePageBounds.Select(p => new PageRowBound(p,verticalBorderOffset,0)));
    }
    else
    {
        var horizontalBorderOffset = this.HorizontalMargin;

        for (int i = 0; i < singlePageBounds.Length; i++)
        {
            if (i == 0 && viewType == MoonPdfLib.ViewType.BookView)
            {
                // in BookView, the first page row contains only one PDF page 
                finalBounds.Add(new PageRowBound(singlePageBounds[0], verticalBorderOffset, 0));
                continue;
            }

            var subset = singlePageBounds.Take(i, pagesPerRow).ToArray();

            // we get the max page-height from all pages in the subset
            // and the sum of all page widths of the subset plus the offset between the pages
            finalBounds.Add(new PageRowBound(new Size(subset.Sum(f => f.Width),
                subset.Max(f => f.Height)), verticalBorderOffset,
                horizontalBorderOffset * (subset.Length - 1)));
            i += (pagesPerRow - 1);
        }
    }

    return finalBounds.ToArray();
}

因此,我们最终计算出了所有页面行的所需空间。我们将这些边界分配给 CustomVirtualizingPanel 对象 (参见下方) 的 PageRowBounds 属性。基于这些边界,我们可以在以后确定 CustomVirtualizingPanel 的总所需空间。这是在 CalculateExtent 方法中完成的。我们取所有页面行的最大宽度,这样我们就知道范围有多宽。然后,我们将所有页面行的总高度相加,以获得所需的总高度。

internal class CustomVirtualizingPanel : VirtualizingPanel, IScrollInfo
{
    ...
    public Size[] PageRowBounds { get; set; }

    private System.Windows.Size CalculateExtent(...)
    {
        ...
        // we get the pdf page with the greatest width, so we know how broad the extent must be
        var maxWidth = PageRowBounds.Select(f => f.Width).Max();

        // we get the sum of all pdf page heights, so we know how high the extent must be
        var totalHeight = PageRowBounds.Sum(f => f.Height);

        return new Size(maxWidth, totalHeight);
    }
    ...
}

将MoonPdfPanel集成到您的应用程序中

MoonPdfPanel 集成到您的应用程序中非常简单。MoonPdfLib 的二进制文件包含三个DLL文件。其中两个是 .NET 程序集 (MoonPdfLib.dllMouseKeyboardActivityMonitor.dll)。另一个DLL (libmupdf.dll) 是一个本地DLL,包含MuPdf的功能。首先,您需要在项目中引用包含 MoonPdfPanel 用户控件的 MoonPdfLib.dll。其次,您需要确保其他两个DLL也位于与引用的 MoonPdfLib.dll 相同的输出文件夹中。

DLL现在已就位。您现在可以在应用程序中访问和定位 MoonPdfPanel。下面的XAML是一个示例。首先,您需要包含 MoonPdfLib 程序集的xml命名空间。这是第一个加粗文本。其次,您可以使用选定的xml命名空间前缀 (在我们的例子中是 mpp) 来包含 MoonPdfPanel。这在第二个加粗文本中显示。下面的XAML还显示了一些设置的依赖属性,例如控件的背景颜色或视图类型。

<Window x:Class="YourApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mpp="clr-namespace:MoonPdfLib;assembly=MoonPdfLib">
    <DockPanel LastChildFill="True">
        <mpp:MoonPdfPanel x:Name="moonPdfPanel" 
        Background="LightGray" ViewType="SinglePage"
                PageDisplay="ContinuousPages" 
                PageMargin="0,2,4,2" AllowDrop="True" />
    </DockPanel>
</Window>

关于属性 PageMarginAllowDrop 的简短说明。PageMargin 分别指定PDF页面和页面行之间的水平和垂直边距。Left 属性的值未使用 (参见上面的0值)。TopBottom 的值用于页面行之前和之后的垂直间距。Right 的值指定PDF页面之间的水平边距。AllowDrop 属性来自基类 UIElement。如果设置为 True,则可以将PDF文件拖放到 MoonPdfPanel 中,它将自动加载。

MoonPdfPanel 放置到位后,有许多 public 方法可以调用。其中最重要的是 OpenFile 方法,它根据完整文件路径加载PDF。之后,您可以调用其他方法,如 ZoomInGotoNextPage。许多功能也可以通过鼠标和键盘访问,特别是缩放和导航功能。探索 MoonPdfPanel 的一个好起点是查看PDF查看器 MoonPdfMoonPdf 包含了 MoonPdfPanel 并创建了一个小的用户界面 (主菜单) 来访问 MoonPdfPanel 的最重要功能。

处理密码保护的PDF文件

MoonPdf 0.2.3版本增加了打开密码保护PDF文件的能力。为了使其工作,您可以在 MoonPdfPanelOpenFile 方法中指定密码。还有一个回调事件,可以用来在打开PDF文件之前请求密码。例如,当用户将文件拖放到用户控件中时,这是必需的。该事件在 MoonPdfPanel 中定义,并称为 PasswordRequired。下面的示例显示了一个例子 (MainWindow.xaml.cs 的摘录)

// defined in the constructor after components are initialized
moonPdfPanel.PasswordRequired += moonPdfPanel_PasswordRequired;

// event / callback method
void moonPdfPanel_PasswordRequired(object sender, PasswordRequiredEventArgs e)
{
    var dlg = new PdfPasswordDialog();

    if (dlg.ShowDialog() == true)
        e.Password = dlg.Password;
    else
        e.Cancel = true;
}

从内存加载PDF文档

MoonPdfPanel 包含以下加载PDF文档的方法

public void Open(IPdfSource source, string password = null)
{
...
}

要从内存打开PDF,您可以使用 MemorySource 类,例如如下所示

MoonPdfPanel p = new MoonPdfPanel();
...
byte[] bytes = File.ReadAllBytes("somefilename.pdf");
var source = new MemorySource(bytes);
p.Open(source);

MoonPdfPanel的当前特性

  • 单页和连续页面显示
  • 查看单页或多页 (对开或书籍视图)
  • 单击并拖动滚动
  • 导航功能,例如转到页面,下一页等
  • 缩放功能,包括“适合高度”和“适合宽度”
  • 旋转功能
  • 拖放功能 (将PDF文件拖放到面板中以打开它们)
  • 打开密码保护的PDF文件
  • 从内存 (byte[]) 打开PDF文档

最终 remarks

在CodeProject潜水近10年 (撰写时9年8个月) 之后,我终于在这里发布了我的第一篇文章 ;-)

我希望您觉得这篇文章和 MoonPdfPanel 有用。我非常感谢您的任何评论和建议。也许您也可以在您的项目中使用 MoonPdfPanel,我很想听到相关信息。

历史

  • 2013年11月28日:增加了从内存 byte[] 打开PDF文档的能力。添加了新版本0.3.0。
  • 2013年9月26日:增加了打开密码保护PDF文件的能力。添加了新版本0.2.3。
  • 2013年5月21日:修复了自定义滚动条的问题。添加了新版本0.2.2。
  • 2013年5月9日:修复了非标准DPI的问题。添加了新版本0.2.1。
  • 2013年4月18日:文章提交。
© . All rights reserved.