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






4.89/5 (33投票s)
本文介绍了MoonPdfPanel的工作原理以及如何将其集成到您的应用程序中
目录
引言
与 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
是两个源 (FileSource
和 MemorySource
) 的公共接口。
大部分非托管资源被封装在 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.BookView
与 ViewType.Facing
相同,只是它以第一个页面行中的单个PDF页面开始。
为了说明视图类型,我们来看下面的图。它显示了MoonPdf中带有 ViewType.Facing
视图类型的示例PDF。因此,图显示了一个包含两个PDF页面的页面行。

除了视图类型之外,第二个重要的布局方面是页面行的显示方式。这种行为由 PageRowDisplayType
枚举处理 (参见下面的代码)。PageRowDisplayType.SinglePageRow
值用于一次只显示一个页面行。这在上面的图中有显示。另一个选项是 PageRowDisplayType.ContinuousPageRows
,它连续显示页面行。这种显示类型的示例显示在 第一个示例图 中。
public enum PageRowDisplayType
{
SinglePageRow = 0,
ContinuousPageRows
}
实现 (用户控件)
这两种页面行类型的布局逻辑差异很大,因此我决定为每种类型实现一个用户控件。我创建了两个用户控件 SinglePageMoonPdfPanel.xaml 和 ContinuousMoonPdfPanel.xaml。尽管它们的行为不同,但它们有一个共同点,即页面行总是包含一到两个PDF页面并排显示。实现这一点的最简单方法是使用 ItemsControl
并将其 ItemsPanel
定义为具有水平方向的 StackPanel
。ItemsControl
的项是 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
,它包含适当的用户控件 (SinglePageMoonPdfPanel
或 ContinuousMoonPdfPanel
),具体取决于所选的 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
,其中将添加适当的用户控件 (SinglePageMoonPdfPanel
或 ContinuousMoonPdfPanel
)。这在 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页面的数据绑定有两个属性 ImageSource
和 Margin
。这些是 PdfImage
类的一部分 (参见下方)。ImageSource
属性用于保存PDF页面的图像,Margin
属性用于定义PDF页面的边距,即页面之间的水平边距 (使用 ViewType.Facing
或 ViewType.BookView
时)。对于 Margin
,只使用了 Thickness
结构的 Right
属性,但我选择 Thickness
结构而不是简单的 double
,因为它使数据绑定更容易。
internal class PdfImage
{
public ImageSource ImageSource { get; set; }
public Thickness Margin { get; set; }
}
加载所需PDF页面的逻辑封装在 PdfImageProvider
类中。该类实现了 Paul 的数据虚拟化解决方案 中的泛型 IItemsProvider
接口。PdfImageProvider
的两个重要方法是 FetchCount
和 FetchRange
(参见下方)。
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
被用作我们 ItemsControl
的 ItemsPanel
。
<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.dll 和 MouseKeyboardActivityMonitor.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>
关于属性 PageMargin
和 AllowDrop
的简短说明。PageMargin
分别指定PDF页面和页面行之间的水平和垂直边距。Left
属性的值未使用 (参见上面的0值)。Top
和 Bottom
的值用于页面行之前和之后的垂直间距。Right
的值指定PDF页面之间的水平边距。AllowDrop
属性来自基类 UIElement
。如果设置为 True
,则可以将PDF文件拖放到 MoonPdfPanel
中,它将自动加载。
将 MoonPdfPanel
放置到位后,有许多 public
方法可以调用。其中最重要的是 OpenFile
方法,它根据完整文件路径加载PDF。之后,您可以调用其他方法,如 ZoomIn
或 GotoNextPage
。许多功能也可以通过鼠标和键盘访问,特别是缩放和导航功能。探索 MoonPdfPanel
的一个好起点是查看PDF查看器 MoonPdf
。MoonPdf
包含了 MoonPdfPanel
并创建了一个小的用户界面 (主菜单) 来访问 MoonPdfPanel
的最重要功能。
处理密码保护的PDF文件
MoonPdf 0.2.3版本增加了打开密码保护PDF文件的能力。为了使其工作,您可以在 MoonPdfPanel
的 OpenFile
方法中指定密码。还有一个回调事件,可以用来在打开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日:文章提交。