Adding a thumbnail view & navigation to the PdfViewer control

If you’ve used the PdfViewer control you’ll know that it provides an easy way to display PDF documents within¬† your WinForms application. Currently, you can select text, manipulate a file (via the PdfDocumentProccessor non-visual component) and even fill out PDF forms. The 14.2.5 release even allows the ability to programmatically fill in PDF forms–pretty useful stuff for those of you processing electronic documents and working towards the dream of a paperless office.

One feature that I find lacking in the PdfViewer control is a good thumbnail navigation system. Adobe Reader offers this ability with a simple thumbnail navigation system; each page of the document is displayed as a small image which users can quickly scroll through. Double-clicking on a thumbnail opens the corresponding page and end users can even change the size of the thumbnail images.

Screenshot of Adobe Reader's thumbnail navigation
Screenshot of Adobe Reader’s thumbnail navigation

The DevExpress PdfViewer will display thumbnails for pages if you zoom out to about the 10-20% zoom level, but the functionality ends there. You can’t double-click one of the pages and have the viewer navigate to that particular page. How can we implement a system similar to Adobe Reader ourselves?

DevExpress's non-functional thumbnail mode.
DevExpress’s non-functional thumbnail mode.

To mimic Adobe Reader’s thumbnail function, I immediately though of a few controls that might make our job easier:

  • GalleryControl – This could easily organize and display our thumbnail images. We don’t really need any grouping functionality but that’s easy to ignore.
  • GridControl – This tends to be my default go-to for displaying a list of items/records. But that’s only the first half of the equation–after that, we’d have to decide which view type to use. The LayoutView in a single column mode would work well, but honestly, I hate the layout editor.

In the end, I decided to use the GridControl, but with the WinExplorerView instead of the LayoutView. The WinExplorerView mimics the various ways you might view a folder in Windows Explorer–details, large icons, small icons etc… This is all controlled via the OptionsViewStyles property, so we can even allow the user to toggle between styles at runtime.

At the end of this exercise, we’ll end up with something like this:

Our semi-finished product: a PdfViewer control with a sidebar of page thumbnails.
Our semi-finished product: a PdfViewer control with a sidebar of page thumbnails.

To being, we’ll create a form and place a LayoutControl onto the form. Dock the LayoutControl to fill the form, and then drop a GridControl onto the LayoutControl, followed by a PdfViewer control to the right. The GridControl can be placed in a LayoutControlGroup and the PdfViewer can just be contained within a standard LayoutControlItem. I also changed the view type of the GridControl from the default GridView to the WinExplorerView type.

The thumbnail logic is all going to be contained within a class I’ve called PdfPage. Let’s have a look at it:

namespace PdfThumbnails
{
    /// <summary>
    /// Encapsulates the properties and methods of a PdfPage
    /// </summary>
    sealed public class PdfPage
    {

        #region Public properties

        /// <summary>
        /// Gets or sets the number of this Pdf Page
        /// </summary>
        public int PageNumber
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the thumbnail image for this Pdf Page
        /// </summary>
        public Image Thumbnail
        {
            get;
            private set;
        }

        #endregion


        /// <summary>
        /// Initializes a new instance of the PdfPage class
        /// </summary>
        public PdfPage()
        {

        }



        /// <summary>
        /// Sets the thumbnail image for the current PdfPage
        /// </summary>
        /// <param name="DocumentViewer">PdfViewer control responsible for displaying the PdfPage</param>
        public void SetThumbnailImage(PdfViewer DocumentViewer)
        {
            int PageWidth = 0;
            int PageHeight = 0;
            int PagePixelWidth = 0;
            int PagePixelHeight = 0;

            using (Graphics graphics = DocumentViewer.CreateGraphics())
            {
                //Get the page dimensions
                PageWidth = (int)DocumentViewer.GetPageSize(PageNumber).Width;
                PageHeight = (int)DocumentViewer.GetPageSize(PageNumber).Height;

                //Convert the page dimensions into screen pixels
                PagePixelWidth = (PageWidth * (int)graphics.DpiX);
                PagePixelHeight = (PageHeight * (int)graphics.DpiY);

                Thumbnail = DocumentViewer.CreateBitmap(PageNumber, Math.Max(PagePixelWidth, PagePixelHeight));

            }   //End the using() statement

        }   //End the SetThumbnailImage class



    }   //End the PdfPage class
}   //End the PdfThumbnails namespace

The class is pretty sparse in and of itself–just two properties. One to keep track of the page number within the document and another to hold the thumbnail image. The important part is the SetThumbnailImage method, which is in charge of creating a thumbnail for this page.

The method requires an instance of the PdfViewer control because unfortunately, the control doesn’t offer a way to get an instance of a single visual page. Luckily, having an instance of the PdfViewer control allows to use its CreateGraphics method to get a reference to the drawing surface which will be useful for generating that thumbnail. And it is useful because the PdfViewer’s GetPageSize method returns the page’s dimensions in inches, not in pixels.

I think this was a poor decision on the part of DevExpress, because it’s not mentioned in the documentation that SizeF struct returned from the method is measured in inches and I have no clue how useful that information would be in metric-based countries. Our demonstration PDF will return 8 & 11 for the page’s width and height, respectively. Since we don’t want to create an 8px X 11px Bitmap, we need to convert these dimensions into pixels.

I’m not a graphics programmer, but I do know that DPI settings will influence the conversion between inches and pixels–after all, its name implies how many dots-per-inch there are on our display! Because of this, we can’t just multiply the page dimensions by some magic number. We have to take this DPI into account and multiply by that instead.

Once we have the page and screen dimensions, it’s a matter of using the PdfViewer’s CreateBitmap method to generate a Bitmap image of current page.

I then create a BindingSource component on the Form and set its DataSource property the PdfThumb class. After that, I set the GridControl’s DataSource to this BindingSource component. The GridControl will automatically read the scheme from this data source, but because we’re using the GridControl with a WinExplorerView there aren’t any columns. Instead, there’s a ColumnSet, which maps properties of the data source to the predefined properties of the WinExplorerView.

Since a future version of this application may allow a user to toggle between thumbnail sizes, we’ll set the values for SmallImageColumn, MediumImageColumnn, LargeImageColumn and ExtraLargeImageColumn members of that ColumnSet property. These are all set to the PdfPage’s ThumbnailImage property, and we’ll set the ColumnSet’s Description andTextColumn properties to the PageNumber property.

At this point, we have a PdfViewer control capable of displaying our PDF, a GridControl capable of displaying page thumbnails and a class capable of generating those thumbnails. The only thing we need to do is actually tell our application to generate those thumbnails at runtime when a PDF is loaded into the viewer.

Create a handler for the the PdfViewer’s DocumentChanged event as this will be fired every time a document is loaded into the PdfViewer control. After that, it’s just a matter of looping through the pages of the PdfViewer and creating an instance of the PdfPage class for each:

/// <summary>
/// Document changed event handler for the PdfViewer control
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void pdfViewer1_DocumentChanged(object sender, DevExpress.XtraPdfViewer.PdfDocumentChangedEventArgs e)
{
    IList<PdfPage> documentPages = new List<PdfPage>(pdfViewer1.PageCount);

    for (int i = 1; i <= pdfViewer1.PageCount; i++)
    {
        PdfPage documentPage = new PdfPage();
        documentPage.PageNumber = i;
        documentPage.SetThumbnailImage(pdfViewer1);

        documentPages.Add(documentPage);

    }   //End the for() loop

    bindingSource1.DataSource = documentPages;

}   //End the pdfViewer1_DocumentChanged() method

Once that’s done, our GridControl will automatically show those page thumbnails when the application is run and a Pdf is loaded into the viewer.

The final step is to handle the WinExplorerView’s DoubleClick event:

/// <summary>
/// Double click event handler for the Thumbnails WinExplorerView
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void wvThumbnails_DoubleClick(object sender, EventArgs e)
{
    PdfPage currentPage = (wvThumbnails.GetFocusedRow() as PdfPage);

    pdfViewer1.CurrentPageNumber = currentPage.PageNumber;

}   //End the wvThumbnails_DoubleClick() method

This code simply takes the current row (thumbnail) from the thumbnail grid and casts it to an instance of our PdfPage class. Since the class is nice enough to keep track of its page number on our behalf, we can tell the PdfViewer to navigate to corresponding page.

And that’s all there is to it! Once small class, a couple of event handlers and we’re done. In an upcoming post, we’ll look at adding a few additional features to this application such as:

  • Allowing the user to change the thumbnail size
  • Synchronizing the thumbnail sidebar scroll position to the PdfViewer scroll position
  • Showing a magnified image of the thumbnail when the user hovers over a thumbnail image

And don’t worry, we’ll also get back to the DxContactList Xpo tutorial soon! In the meantime, here is the full source code for this tutorial: PdfThumbnails.

Adding a thumbnail view & navigation to the PdfViewer control

Extending XtraEditors for fun and profit

One question that I see pop up every now and then in the DevExpress Support Center deals with changing the default behavior of the various controls offered in the XtraEditors suite. Many people are looking for a static property that can override the default settings of the controls so that they don’t need to constantly set the same properties for controls that are used over and over again. While there are some static properties and methods that can control behavior, for the most part you’re on your own when it comes to changing how the editors work on an application-wide basis.

To make things easier, I often find myself extending the DevExpress controls to suit my own requirements. The various controls offered in the XtraEditors suite are for the most part descendants of the standard .NET controls offered in System.Windows.Forms namespace; for instance, the XtraEditors TextEdit is just a descendant of the System.Windows.Forms.TextBox control, albeit with a lot of additional functionality and styling added.

The simplest method of building our own extended control library would be to just create a class that derives from one of the DevExpress controls:

public class MyTextEdit : DevExpress.XtraEditors.TextEdit
{
    public MyTextEdit() : base() { }
}

So with that, we have the beginnings of a basic extension of the TextEdit.

I’ve come to embrace the idea of changing user-input into upper case for the sake of consistency. To handle this, the TextEdit control offers a nifty feature in the form of the CharacterCasing property. You can set this property to CharacterCasing.Upper for each instance of the TextEdit control to automatically change the user’s input to upper case regardless of their CAPS LOCK setting.

Doing this is easy enough, but it kind of violates the principle we’re trying to establish here. If you have a dozen forms, each containing 10 TextEdit controls, do you really want to repetitively change the same property over and over? Instead, let’s build a basic TextEdit descendant that automatically defaults to upper case!

    /// <summary>
    /// A TextEdit control which defaults to upper case text editing
    /// </summary>
    [ToolboxItem(true)]
    [Description("Creates a TextEdit control which defaults to upper case text editing")]
    public class UpperCaseTextEdit : TextEdit
    {

        /// <summary>
        /// Initializes a new instance of the UpperCaseTextEdit class
        /// </summary>
        public UpperCaseTextEdit()
            : base()
        {
        }



        /// <summary>
        /// OnCreateControl event handler
        /// </summary>
        protected override void OnCreateControl()
        {
            base.OnCreateControl();

            Properties.CharacterCasing = System.Windows.Forms.CharacterCasing.Upper;

        }   //End the OnCreateControl() method


    }   //End the UppercaseTextEdit class

Let’s have a look at what’s going on here.

First, we’ve decorated our class with the [ToolboxItem(true)] attribute. This tells Visual Studio that the control should be made available in the toolbox window. Visual Studio is also nice enough to automatically look through your solution for any controls which have this attribute and place them in the toolbox for immediate use.

Aside from that, the most important thing to do is to place your default behavior within the control’s OnCreateControl event handler. If you don’t, you may find that your settings are just overwritten at runtime.

This is all and good, but many times we edit our data within a GridControl. If we want our new UpperCaseTextEdit control to be usable as a GridControl repository item, we’ll need to define our own repository item type.


    /// <summary>
    /// 
    /// </summary>
    [UserRepositoryItem("Register")]
    public class RepositoryItemUpperCaseTextEdit : RepositoryItemTextEdit
    {

        #region Internal members

        /// <summary>
        /// Editor name
        /// </summary>
        internal const string EditorName = "UpperCaseTextEdit";

        #endregion

        #region Public accessors

        /// <summary>
        /// Gets the EditorTypeName of this RepositoryItemTextEdit editor
        /// </summary>
        public override string EditorTypeName
        {
            get { return EditorName; }
        }

        #endregion


        /// <summary>
        /// Static constructor
        /// </summary>
        static RepositoryItemUpperCaseTextEdit()
        {
            Register();
        }


        /// <summary>
        /// Initializes a new instance of the RepositoryItemUpperCaseTextEdit class
        /// </summary>
        public RepositoryItemUpperCaseTextEdit()
            : base()
        {

        }


        public override System.Windows.Forms.CharacterCasing CharacterCasing
        {
            get
            {
                return System.Windows.Forms.CharacterCasing.Upper;
            }
            set
            {
                base.CharacterCasing = value;
            }
        }


        /// <summary>
        /// Registers this editor with the designer
        /// </summary>
        public static void Register()
        {
            EditorRegistrationInfo.Default.Editors.Add
                (
                    new EditorClassInfo(EditorName, typeof(UpperCaseTextEdit),
                    typeof(RepositoryItemUpperCaseTextEdit), typeof(TextEditViewInfo),
                    new TextEditPainter(), true, null)
                );

        }   //End the Register() method

    }   //End the RepositoryItemUpperCaseTextEdit class

Most of this code is going to be pretty boiler-plate as you build your own extended control library. You must provide an EditorTypeName, register it in a static constructor and provide an implementation of the Register method. The Register method is going need to know the name of the editor, the type (this is your control class) and what DevExpress ViewInfo and Painter to use. How do I know which ViewInfo or Painter to use? For the most part, it’s pretty self-explanatory, but there are some caveats. For instance, a LookUpEdit descendant needs to use the ButtonEditPainter. Lucky for us, DevExpress provides a handy chart viewable in the Custom Editors documentation.

One more thing of note: you’ll notice that I’ve overrode the CharacterCasing property in the RepositoryItemUpperCaseTextEdit class to have it return CharacterCasing.Upper by default. This is a better solution than setting the property in the OnCreateControl event.

What if we’d like to create a control with some additional properties? When my users need to enter a location address, I present them with a drop-down list containing the abbreviations of the US and Mexican states as well as Canadian provinces. After all, who can remember that “MI” is Michigan and not Missouri? To simply this data entry, let’s create a ComboBoxEdit descendant which is pre-filled with these abbreviations and has some additional properties to determine if we want to show Mexican states or Canadian provinces.

We’ll start by creating a control that derives from the ComboBoxEdit class and we’ll add in a few new members:


        /// <summary>
        /// Include USA flag
        /// </summary>
        private bool _IncludeUSA;

        /// <summary>
        /// Include Canada flag
        /// </summary>
        private bool _IncludeCanada;

        /// <summary>
        /// Include Mexico flag
        /// </summary>
        private bool _IncludeMexico;

That helps us within the class, but we are going to want those properties to be exposed at design-time via the Visual Studio properties window. Let’s add some public accessors to expose the private members:


        /// <summary>
        /// Gets or sets a value indicating if US states should be included in the ComboBoxEdit
        /// </summary>
        [Browsable(true)]
        [PropertyTab("Geography", PropertyTabScope.Component)]
        [Description("Gets or sets a value indicating if US states should be included in the ComboBoxEdit")]
        public bool IncludeUSA
        {
            get { return _IncludeUSA; }
            set { _IncludeUSA = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating if Canadian provinces should be included in the ComboBoxEdit
        /// </summary>
        [Browsable(true)]
        [PropertyTab("Geography", PropertyTabScope.Component)]
        [Description("Gets or sets a value indicating if Canadian provinces should be included in the ComboBoxEdit")]
        public bool IncludeCanada
        {
            get { return _IncludeCanada; }
            set { _IncludeCanada = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating if Mexican states should be included in the ComboBoxEdit
        /// </summary>
        [Browsable(true)]
        [PropertyTab("Geography", PropertyTabScope.Component)]
        [Description("Gets or sets a value indicating if Mexican states should be included in the ComboBoxEdit")]
        public bool IncludeMexico
        {
            get { return _IncludeMexico; }
            set { _IncludeMexico = value; }
        }

The important take-away from the above snippet is to decorate your properties with the [Browsable(true)] attribute. Without this, Visual Studio will not display these properties in the designer.

If you want to pre-populate a list editor, do so within the OnLoaded event:

        /// <summary>
        /// On loaded event handler
        /// </summary>
        protected override void OnLoaded()
        {
            Properties.TextEditStyle = DevExpress.XtraEditors.Controls.TextEditStyles.DisableTextEditor;
            LoadStates(_IncludeUSA, _IncludeCanada, _IncludeMexico);

            base.OnLoaded();

        }   //End the OnLoaded() method

Here you’ll see that we’re disabling the ability for end-users to enter their own text within the drop-down by default. We certainly don’t want them inventing their own state/province abbreviations! From there, we invoke the LoadStates method and pass in our Boolean flags that indicate what should be contained within the drop-down. I chose to NOT populate the list within this event because I’d like to be able to reload the list at runtime; for instance, if we have another drop down called “Country”, it doesn’t make sense to show Canadian provinces if the user selects “United States”. At runtime, I can call the StateComboBoxEdit’s LoadStates method to change what is visible in the list.

The LoadStates method just builds a basic object array to be assigned to the control’s Items collection:

        /// <summary>
        /// Populates the State ComboBoxEdit with states/provinces
        /// </summary>
        /// <param name="includeCanada">True to include Canadian provinces</param>
        public void LoadStates(bool includeUSA, bool includeCanada, bool includeMexico)
        {
            //Clear existing items
            if (DesignMode == true || Properties.Items.Count > 0)
                Properties.Items.Clear();

            //Add states/provinces
            if (includeUSA == true)
            {
                Properties.Items.AddRange(new object[] 
                { 
                    "AK",
                    "AL",
                    "AP",
                    "AR",
                    "AS",
                    "AZ",
                    "CA",
                    "CO",
                    "CT",
                    "DC",
                    "DE",
                    "FL",
                    "FM",
                    "GA",
                    "GU",
                    "HI",
                    "IA",
                    "ID",
                    "IL",
                    "IN",
                    "KS",
                    "KY",
                    "LA",
                    "MA",
                    "MD",
                    "ME",
                    "MH",
                    "MI",
                    "MN",
                    "MO",
                    "MP",
                    "MS",
                    "MT",
                    "NB",
                    "NC",
                    "ND",
                    "NE",
                    "NF",
                    "NH",
                    "NJ",
                    "NM",
                    "NT",
                    "NU",
                    "NV",
                    "NY",
                    "OH",
                    "OK",
                    "OR",
                    "PA",
                    "PR",
                    "PW",                
                    "RI",
                    "SC",
                    "SD",                
                    "TN",
                    "TX",
                    "UT",
                    "VA",
                    "VI",
                    "VT",
                    "WA",
                    "WI",
                    "WV",
                    "WY"                
                });
            }

            if (includeCanada == true)
            {
                Properties.Items.AddRange(new object[]
                {
                    "AA",
                    "AB",
                    "AE",
                    "BC",
                    "MB",
                    "NS",
                    "ON",
                    "PE",
                    "PQ",
                    "QC",
                    "SK",
                    "YT"
                });
            }

        }   //End the LoadStates() method

The only thing of note in this method is that I first check if we’re in design-mode or if the list already has items in it. If you don’t do this you’ll find that every time the list is opened (or created as you switch from design view to code view and back-and-forth) your items will be added again. This is obviously not ideal, so take care to always perform this check with list editors!

Take some time and look at your application to see what editors are duplicated to perform common tasks and you may find that it’s worthwhile to create your own control assembly. Personally, I’ve built some useful editors such as:

  • CountryComboBoxEdit – a basic drop down with a list of countries
  • DataLayoutControlEx – an extension of the DataLayoutControl which disables end-user manipulation by default, is automatically set to DockStyle.Fill, focuses an editor when its corresponding caption is clicked (similar to a <label> tag in Html) and overrides the default behavior to use my UpperCaseTextEdit in lieu of the TextEdit when an item is bound to a string type.
  • EmailAddressEdit and UrlEdit :¬† ButtonEdit descendants which have a default button that opens the user’s email editor with the control’s text set as the mail to, or opens a browser with the provided Url.
  • PhoneNumberEdit: a TextEdit control which has a default RegEx mask set for North American phone numbers.

Download the full source code for our examples here and experiment! CustomEditors

Extending XtraEditors for fun and profit