Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

MoonPdfPanel - A WPF-based PDF Viewer Control

4.89/5 (33 votes)
26 Sep 2013GPL317 min read 207.1K   8.6K  
This article describes how the MoonPdfPanel works and how you can integrate it in your application

MoonPdf Sample Image

Table of Contents

Introduction

Similar to wmjordan, who wrote the CodeProject article Rendering PDF Documents with Mupdf and P/Invoke in C#, I was looking for a free, native .NET PDF rendering engine. Like him, I did not find any and so I used his solution, which uses MuPdf to render PDF pages as images.

Based on his code, I wrote the WPF user control MoonPdfPanel, which can be used to display PDF files in a .NET based application with minimal effort. To demonstrate the use of MoonPdfPanel, I wrote a sample WPF application named MoonPdf. MoonPdf can be considered as a very basic PDF viewer/reader. It uses the MoonPdfLib assembly, which contains the mentioned MoonPdfPanel. The above screenshot shows the MoonPdf application with a loaded sample PDF file.

In this article, I will show how the MoonPdfPanel works and how you can integrate it in your application to display PDF files.

Related and Helpful Projects

There are two CodeProject articles that helped me a lot with the creation of MoonPdf:

As stated above, the first article helped me with the usage of MuPdf for rendering PDF pages as images. The second article provided a useful solution for data virtualization in WPF. This code was used to implement a virtualizing panel, that made a continuous page layout for PDF pages possible. It allowed me to virtualize the PDF pages, i.e. it is not necessary to load all pages at once. This increased performance and decreased memory consumption. I will explain the details on these implementations later.

Building and Including the MuPdf Rendering Engine

For the rendering of the PDF pages, I used the MuPdf rendering engine. MuPdf is also used in the well known PDF reader SumatraPDF. The guys of SumatraPDF already did a great job in providing nmake files that build a DLL from the MuPdf sources. So in the end, I included the source code of SumatraPDF to build a MuPdf DLL (libmupdf.dll). This DLL is necessary to use the solution proposed in Rendering PDF Documents with Mupdf and P/Invoke in C#.

To include the DLL in the build process, I wrote a small nmake file, that builds and copies the libmupdf.dll accordingly. The following source code shows the msbuild file, that is used to compile the complete source code. Before the MoonPdf solution is built, the MuPdf sources are built using my nmake file makefile-mupdf.msvc (not shown here). After that, the compiled libmupdf.dll (bold in the sources below) is copied accordingly.

XML
<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>

Rendering PDF Pages

The rendering of the PDF pages is pretty straight forward. It all takes place in the MuPdfWrapper class. The main part is done in the ExtractPage method, which is shown below. The method expects an IPdfSource object (see below code), the page number that should be rendered and the zoom factor that should be applied for the rendering. The method returns a Bitmap object, which is later on converted into a BitmapSource object, to use it in WPF (see the next section).

C#
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 allows loading PDF documents from file or from memory. The above mentioned interface IPdfSource is the common interface for the two sources (FileSource and MemorySource).

Most of the unmanaged resources were encapsulated into the class PdfFileStream, which implements the IDisposable interface. The use of the PdfFileStream object is shown in the using statement above. For the rendering, the ExtractPage method makes use of the RenderPage method. This method (and the rest of the interop code) can be looked up here. I only made a slight modification to the RenderPage method, to take the zoom factor into account. The modifications are shown below. I omitted the rest of the (not so short) method for clarity reasons.

C#
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;
    
    ...
}

Well, that's pretty much everything about the rendering of the PDF pages. Later, I will show where the ExtractPage method is called, to display the rendered images.

Displaying the Rendered PDF Pages

Basics

Before I explain further details, I need to clarify some terms that I will use further on:

  • PDF page: A page from the PDF document that gets rendered as a bitmap.
  • Page row: A collection of one or two PDF pages (two PDF pages are displayed side by side).

In MoonPdf, the view types of a page row are addressed with the enum ViewType:

C#
public enum ViewType
{
    SinglePage,
    Facing,
    BookView
}

ViewType.SinglePage is the simplest case, where a PDF page and a page row are identical, which means that one page row only contains one PDF page. ViewType.Facing means that every page row contains two PDF pages (except there is only one PDF page left to fit in the page row). ViewType.BookView is the same as ViewType.Facing, except that it starts with a single PDF page in the first page row.

To illustrate the view types, we look at the following figure. It shows a sample PDF in MoonPdf with the view type ViewType.Facing. The figure therefore shows one page row with two PDF pages.

Image 2

Besides the view type, the second important layout aspect is the way the page rows are displayed. This behaviour is adressed with the enum PageRowDisplayType (see code below). The value PageRowDisplayType.SinglePageRow is used to display only one page row at a time. This is shown in the figure above. The other option is PageRowDisplayType.ContinuousPageRows, which shows page rows continuously. An example for this display type is shown in the first sample figure.

C#
public enum PageRowDisplayType
{
    SinglePageRow = 0,
    ContinuousPageRows
}

Implementation (user controls)

The layout logic for the two page row types is very different, so I decided to implement a user control for each of these types. I created the two user controls SinglePageMoonPdfPanel.xaml and ContinuousMoonPdfPanel.xaml. Although their behaviour is different, they have one thing in common, namely that a page row always contains one or two PDF pages side by side. The easiest way to implement this was to use a ItemsControl and define its ItemsPanel as a StackPanel with horizontal orientation. The items of the ItemsControl would be Image objects that would contain the rendered PDF pages as images. I encapsulated the common XAML for both user controls into a global ResourceDictionary named GlobalResources.xaml, so that it can be used by both user controls. This XAML is shown below. The XAML also contains data bindings for the source and the margin of the image. I will explain their use later.

XML
<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>

The layout for the SinglePageMoonPdfPanel is pretty straight forward, because it only contains one page row at a time. Therefore we only need one ItemsControl that manages the PDF pages of this one page row. The ItemsControl and the style that it uses (see XAML above) are in bold in the XAML below. The ControlTemplate uses a ScrollViewer element that allows the scrolling of the content. On the ScrollViewer I set the FocusVisualStyle to {x:Null}, to remove the dashed rectangle around the control that is normally shown when the control is focused.

XML
<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>

Compared to the above SinglePageMoonPdfPanel, the ContinuousMoonPdfPanel contains multiple page rows, so here we need an additional ItemsControl that manages the multiple page rows. The code below shows the XAML of the ContinuousMoonPdfPanel. I have highlighted the two ItemsControls in bold that are used. The first one is responsible for the page rows. The second one is responsible to display the images side by side. It uses the style with the key moonPdfItems from the GlobalResources.xaml.

XML
<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>

An interesting part of tabove code is the use of the CustomVirtualizingPanel (in bold). This panel inherits from System.Windows.Controls.VirtualizingPanel and allows us to virtualize page rows. This means we only need to load the current page rows into memory. This also allows us to dispose previous page rows, when the user scrolls further in the document. This virtualizing panel is so important, because in a "normal" items panel, we would have to load and add all the PDF pages before displaying them. This would increase memory consumption dramatically and our application would be unusable for bigger PDF files. But with virtualization, the memory consumption stays within borders. The virtualization will be discussed later and I'll continue to explain a bit more about the controls.

Because I created two separate controls for single page and continuous layout, I needed a way to wrap them in one user control that can be integrated easily. So I created some kind of a "wrapper" panel named MoonPdfPanel, which includes the appropriate user control (SinglePageMoonPdfPanel or ContinuousMoonPdfPanel) depending on the chosen PageRowDisplayType. To make this possible, the two mentioned user controls needed some common base or interface, so I decided to create an interface IMoonPdfPanel that is implemented by these two user controls. The interface is shown below.

C#
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);
}

The interface is implemented by the following classes:

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

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

The wrapper panel MoonPdfPanel has only a reference to the common interface IMoonPdfPanel. The MoonPdfPanel delegates the actions, e.g. zoom or navigation, to the current instance of IMoonPdfPanel. An example is shown in the code below.

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

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

The XAML for the wrapper panel MoonPdfPanel is shown below:

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

As you can see, the XAML is plain simple. The user control only contains a DockPanel where the appropriate user control (SinglePageMoonPdfPanel or ContinuousMoonPdfPanel) will be added to. This is shown below in the code behind the MoonPdfPanel. Whenever the PageRowDisplayType changes, the current innerPanel is removed from the dockpanel (pnlMain). Then a new instance is created (depending on the PageRowDisplayType) and it is added to the dockpanel.

C#
// 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);

Implementation (Data Binding and Virtualization)

As shown in an earlier XAML above, the data binding for the PDF pages has two properties ImageSource and Margin. These are part of the class PdfImage (see below). The ImageSource property is for holding the image of the PDF page and the Margin property is for defining the margins of the PDF pages, i.e., the horizontal margin between two pages (when using ViewType.Facing or ViewType.BookView). For the Margin, only the Right-property of Thickness is used, but I choose the Thickness structure instead of a simple double, because it makes data binding easier.

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

The logic for loading the required PDF pages is encapsulated in the class PdfImageProvider. This class implements the generic IItemsProvider interface from Paul's data virtualization solution. The two important methods of PdfImageProvider are FetchCount and FetchRange (see below).

C#
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 };
                ...
            }
        }
    }
    ...
}

The first method is relevant for the data virtualization used in the ContinuousMoonPdfPanel. It returns the number of the virtualized items. In our case, this is the number of pages in a PDF document. This number can be easily be determined with the help of the MuPdfWrapper, which offers the CountPages method.

The other method FetchRange is for retrieving the PdfImages to display. The code above shows the call of the ExtractPage method to get the bitmap of the respective PDF page. This bitmap is then converted to a BitmapSource object, with help of the custom extension method ToBitmapSource (not shown here). On this BitmapSource object, we then call the Freeze method, to make it unmodifiable. This is important, because for the ContinuousMoonPdfPanel we use Paul's AsyncVirtualizingCollection (see here), which calls the FetchRange asynchronously on a different thread. Because the BitmapSource object is not created on the UI-thread, the later binding (which happens on the UI-thread) would fail, if we would not call the Freeze method.

After the above steps, a new PdfImage object is created and populated with the according values. The FetchRange method returns a list of page rows, where one page row is expressed as an IEnumerable<PdfImage> object. This list is used later for data binding (see below).

When the FetchRange method is called from the SinglePageMoonPdfPanel, only the first item of the list (the first page row) is needed, because this panel only shows one page row at a time. This looks like this:

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

You can see from the method call of FirstOrDefault, that we are only interested in the first item of the result list, which is an object of type IEnumerable<PdfImage>.

When we are using the ContinuousMoonPdfPanel, we are not explicitly calling the FetchRange method. Instead we are making use of the generic AsyncVirtualizingCollection from Paul's article. This class manages the virtualization. It expects an object of IItemsProvider, which we provide with an object of PdfImageProvider. The FetchRange method (which is part of the IItemsProvider interface), will be called implicitly by the AsyncVirtualizingCollection object whenever new items are requested (this happens for example when the user scrolls through the document). The following line shows how the items source is assigned in the ContinuousMoonPdfPanel.

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

One important part to make the virtualization work, is the CustomVirtualizingPanel that was mentioned above. Here is an excerpt that shows the relevant part of above XAML. The first bold text shows the XML namespace to reference the clr namespace, to include the user control. The second bold text shows that the CustomVirtualizingPanel is used as the ItemsPanel of our ItemsControl.

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

Because the items are virtualized, the CustomVirtualizingPanel needs to know how much space is required by the items, i.e. what is the max width and total height of all the virtualized items. This is needed for a correct behaviour of the scrollviewer. The first step to calculate the required space was to get the bounds of all PDF pages for a given document. This was done through the MuPdfWrapper, which uses the native BoundPage methode, which gives us a Rectangle for a given PDF page. The method below gets all the page bounds as Size[]. It also takes a possible rotation of the pages into account. If no rotation or an 180 degree rotation is specified, we need not change anything. But otherwise we switch the width and height of the bounds (see bold text). This was achieved here via the delegate sizeCallback.

C#
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;
    }
}

But knowing the bounds of the PDF pages is only half the story, because we need to take into account, that it is possible to display two PDF pages in one page row, which would result in a different required space. So after knowing the bounds of the single PDF pages, we calculate the required space for all the page rows. This is done in the method CalculatePageRowBounds (see below). The required width of a page row is the sum of the width of the relevant PDF pages plus the chosen horizontal margin between them. The required height of a page row is the maximum of the relevant PDF pages plus the chosen vertical margin.

Example: Let's say for example that the ViewType.Facing (two PDF pages in a page row) is chosen and the size of both PDF pages is 600x800 pixel (width x height) and the horizontal and vertical margins are both 4 pixel. Then the calculated size of the page row would be 1204x1604 pixel. This calculation must be done for all page rows, because it is possible that some PDF pages are wider or higher than others.

C#
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();
}

So we have finally calculated the required space for all the page rows. We assign these bounds to the PageRowBounds property of the CustomVirtualizingPanel object (see below). Based on these bounds, we can later determine the total required space for CustomVirtualizingPanel. This is done in the CalculateExtent method. We take the maximum width of all page rows, so we know how broad the extent must be. We then summarize the heights of all page rows to get the total height that is required.

C#
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);
    }
    ...
}

Including MoonPdfPanel in your Application

Including the MoonPdfPanel in your application is easy. The binaries for MoonPdfLib contain three DLL files. Two of them are .NET assemblies (MoonPdfLib.dll and MouseKeyboardActivityMonitor.dll). The other DLL (libmupdf.dll) is a native dll and contains the MuPdf functionality. First of all, you need to add a reference in your project to the MoonPdfLib.dll, which contains the MoonPdfPanel user control. Secondly, you need to make sure that the other two dlls are also in the same output folder as the referenced MoonPdfLib.dll.

The DLLs are now in place. You can now access and position the MoonPdfPanel in your application. An example for this is shown in the xaml below. First of all, you need to include the xml namespace for the MoonPdfLib assembly. This is the first bold text. Secondly, you can include the MoonPdfPanel with the chosen xml namespace prefix (in our case mpp). This is shown in the second bold text. The XAML below also shows some dependency properties that are set, for example the background color of the control or the view type.

XML
<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>

A short notice for the properties PageMargin and AllowDrop. PageMargin specifies the horizontal and vertical margins between PDF pages and page rows, respectively. The value for the Left property is not used (see the value 0 above). The value for the Top and Bottom are for the vertical spaces before and after a page row. The value for Right specifies the horizontal margin between PDF pages. The AllowDrop property stems from the base class UIElement. If it is set to True, it is possible to drag and drop a PDF file into the MoonPdfPanel and it will be automatically loaded.

With the MoonPdfPanel in place, there are many public methods that can be called. The most important one is the OpenFile method, which loads a PDF based on a full file path. After that, you can call other methods like, ZoomIn or GotoNextPage. Many functionalities can also be access via mouse and keyboard, especially zooming and navigation functions. A good starting point to explore the MoonPdfPanel is to look at the PDF viewer MoonPdf. MoonPdf includes the MoonPdfPanel and creates a small user interface (main menu) to access the most important functions of the MoonPdfPanel.

Handling password protected PDF files

MoonPdf version 0.2.3. added the ability to open password protected PDF files. For that to work, you can specify the password in the OpenFile method of MoonPdfPanel. There is also a callback event which makes it possible to ask for the password before opening the PDF file. This is for example necessary, when the user drags a file into the user control. The event is defined in MoonPdfPanel and is called PasswordRequired. An example is shown below (excerpt of MainWindow.xaml.cs):

C#
// 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;
}

Loading PDF documents from memory

The MoonPdfPanel contains the following method to load PDF documents:

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

To open a PDF from memory, you can use the MemorySource class, for example like this:

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

Current features of MoonPdfPanel

  • Single page and continuous page display
  • View single pages or multiple pages (facing or book view)
  • Click and drag scrolling
  • Navigation functionality, e.g. goto page, next page, etc.
  • Zoom functionality, including "Fit to height" and "Fit to width"
  • Rotate functionality
  • Drop functionality (drop PDF files into the panel to open them)
  • Open password protected PDF files
  • Open PDF documents from memory (byte[])

Final remarks

After lurking on CodeProject for almost 10 years (9 years and 8 month at the time of writing), I finally published my first article here ;-).

I hope you find the article and the MoonPdfPanel useful. I appreciate any of your comments and suggestions. Maybe you can even use the MoonPdfPanel in some of your projects, then I would be very interested to hear about it.

History

  • 28 November 2013: Added the ability to open a PDF document from memory byte[]. Added new version 0.3.0.
  • 26 September 2013: Added the ability to open password protected PDF files. Added new version 0.2.3.
  • 21 May 2013: Fixed an issue with custom scrollviewers. Added new version 0.2.2.
  • 9 May 2013: Fixed an issue with non-standard DPI. Added new version 0.2.1.
  • 18 April 2013: Article submitted.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)