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

Creating a sample WinForms application, part one

I thought that perhaps a good way to expose new DevExpress developers to the various products and controls might be to create a small, simple application. Borne from this idea is DxContactList, a basic application designed to be a digital address book. I came up with some simple functionality that help you become more familiar with some different controls & products:

  • XtraEditors as well as creating some custom controls
  • XtraGrid
  • XtraMap
  • XtraReports
  • eXpress Persistent Objects (Xpo), DevExpress’ ORM product that will handle our data-access and mapping.
  • XtraRichEdit control

I’ve already built the application and I hope to present it to you over the next few weeks in a series of posts. We’ll take a look at each portion of the application, deconstruct how I’ve built it and point out some tips and tricks to make your life easier if you’re trying to develop something similar.

I’ll be starting with the back-end & business logic which is chiefly comprised of our persistent classes. You way want to review some of the Xpo documentation to familiarize yourself with it first. Additionally, I find that the Xpo Best Practices knowledge base article is valuable in helping you avoid some common mistakes as you begin to learn.

Creating a sample WinForms application, part one

Visualizing data with the XtraMap control

One important tool in my company’s flagship software offers users the ability to search for trucks and visualize that information on map to see what is available and where it is. It’s easy to display results to a grid control, but looking at tabular data over and over can be a little boring. Mixing in another control, such as the XtraMap control provides us with the ability to visualize these results in an easy-to-understand & interactive manner.

Here’s a basic overview of how it might look in my software:

XtraMap example
XtraMap example

There’s a basic set of editors in which a user can enter some search criteria and then search results are displayed with a GridControl to the left, and an XtraMap to the right. Clicking on a row show zoom us to the selected result marker on the map and vice versa. We should also make use of the tooltip ability for map markers to display some additional information when a user hovers over a marker.

To simplify things, I’ve provided a basic Xml data source with the project file that will have our “search” results. To get started, we’ll first drop an XtraLayoutControl onto the form to help us with arranging our controls.

I’ve created a LayoutGroup at the top of the form to arrange where our search criteria controls would (we’ll just execute the search with a button since our results are hardcoded), and the bottom portion of the form will have another LayoutGroup containing the GridControl and XtraMap control. Drop both of these onto your form via the VS toolbox–both are contained within the Data & Analytics tab. At this point, we have the skeleton of our interface:

form1Out of the box, the XtraMap control offers the ability to connect to 2 different map providers: Bing Maps and OpenStreetMap. I’d advise you to consult both of these providers to learn the terms and conditions of using their services. Bing Maps will require a developer account; OpenStreetMap will work immediately without the need for an account. For this reason, will choose OpenStreetMap as our provider for this example.

One common topic that pops up on the DevExpress Support Center concerns the ability to connect the XtraMap control to Google Maps. While it’s technically possible (after all, the XtraMap control simply issues an HttpWebRequest to retrieve map tiles and then displays them), it does violate Google’s terms of service for their mapping service and is beyond the scope of this example.

To connect to OpenStreetMap, click on the MapControl’s Smart Tag and click on the “Connect to OpenStreetMap Server” link:

connecttoopenmapAt this point, your XtraMap control is fully operational. Start your project and you will see that map tiles are loaded and you have the ability to pan and zoom your map. When you connected to OpenStreetMap, the XtraMap control automatically generated an Image Tiles Layer for your control. This single layer is responsible for arranging the images returned from OpenStreetMap and draws our map.

From here, we’ll handle the SimpleButton’s Click event, load the Xml into a DataSet and assign it to the GridControl’s DataSource property. We now have a grid with search results, but that’s only half of what we want. We’d like to display a little icon in the map that corresponds to each of the results, so how is that accomplished?

First, we need add another layer to our MapControl. For our purposes, we’ll use the VectorItemsLayer type. I’d recommend reviewing the Layers documentation to see a quick overview of the various layer types available and when & how you should use them.

This can be done via the designer or at runtime; we’ll choose the latter just so we can see what is happening:

/// <summary>
/// Creates a Vector Items Layer on which to display equipment icons
/// </summary>
private void CreateVectorLayer()
{
    VectorItemsLayer itemsLayer = new VectorItemsLayer();
    ListSourceDataAdapter dataAdapter = new ListSourceDataAdapter();

    dataAdapter.Mappings.Latitude = "OriginLatitude";
    dataAdapter.Mappings.Longitude = "OriginLongitude";
    itemsLayer.ItemImageIndex = 0;

    //Map the attributes used for the tooltip
    dataAdapter.AttributeMappings.Add(new MapItemAttributeMapping() { Member = "CompanyName", Name = "CompanyName" });
    dataAdapter.AttributeMappings.Add(new MapItemAttributeMapping() { Member = "MCNumber", Name = "MCNumber" });
    dataAdapter.AttributeMappings.Add(new MapItemAttributeMapping() { Member = "MatchID", Name = "MatchID" });
    dataAdapter.AttributeMappings.Add(new MapItemAttributeMapping() { Member = "CompanyContact.ContactMethodValue", Name = "ContactMethod" });

    //Set up the tooltip pattern for the truck icons
    itemsLayer.ToolTipPattern = "<b>{CompanyName}</b>\r\nMC/DOT #{MCNumber}\r\n{ContactMethod}";
    itemsLayer.Data = dataAdapter;
    mapControl1.Layers.Add(itemsLayer);

}   //End the CreateVectorLayer() method

Let’s break it down:

As expected, we create an instance of the VectorItemsLayer class and additionally an instance of the ListSourceDataAdapter class. The ListSourceDataAdapter is what is responsible for providing data to the XtraMap and shares the GridControl’s data source. Before we provide all that, we’ll see at basic properties to tell it what to do with that data that it will be receiving:

  • Mappings.Latitude – We set this the property in our data source (remember that Xml file?) which contains the latitude co-ordinates.
  • Mappings.Longitude – Similar to above, we set this to the property in the data source containing the longitude co-ordinates.
  • ItemImageIndex – I’ve created an ImageCollection component containing any images that I’d like to draw onto the map. Right now this is just a single png image and I’ve set the XtraMap control’s ImageList property to this ImageCollection. I then tell the VectorItemsLayer which image within the collection is going to correspond to our data source items.
  • I create a collection of MapItemAttributeMapping objects which maps items in the data source to attributes of a MapItem. It’s a little confusingly named since we’re dealing with Maps and mappings. We can create as many attributes as we’d like and connect them to a property in the data source. The Member property needs to match a property/field in the data source, and the Name property is what the VectorItemsLayer will use to identify it.
  • We create a tooltip pattern to be used when our users hover over one of these map items. Some limited HTML is supported (you’ll need to use a ToolTipController component for this) and we place our MapItemAttribute names within { } brackets as placeholders to be populated at runtime when the tooltip is created.
  • Finally, we set the data source for the VectorItemsLayer to this ListSourceDataAdapter and add it all to the MapControl.

We’ll invoke this method from the SimpleButton’s click handler after we set the GridControl’s data source. If we run the application now we’d expect both the GridControl and the XtraMap control to show the search results. This doesn’t happen though, because we never actually set the data source for the ListSourceDataAdapter! We told it WHAT to display and we told the VectorImageLayer HOW to display it, but we’re missing that one last link:

(itemsLayer.Data as ListSourceDataAdapter).DataSource = (grdResults.DataSource);

The GridView’s DataSourceChanged event is a good place to use that code to provide data for the layer’s data adapter.

Now when we run the application, our truck image appears for each of the results and the tooltips are nicely formatted to match the ToolTipPattern we created.

Let’s add in that synchronization between the map and grid that we talked about in the beginning. The first thing we want to handle is when a user clicks on an item in the MapControl. The MapControl exposes a MapItemClick event that will be fired when a user clicks on the truck icons. I’ll handle this event, find the item in the GridControl that matches what the user clicks on and focus that.

/// <summary>
/// Map Item click event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void mapControl1_MapItemClick(object sender, MapItemClickEventArgs e)
{
    if (IsItemSelecting == true)
        return;

    string MatchID = e.Item.Attributes["MatchID"].Value.ToString();
    int RowHandle = gvResults.LocateByDisplayText(0, gvResults.Columns["MatchID"], MatchID);

    if (gvResults.IsValidRowHandle(RowHandle))
    {
        IsItemSelecting = true;
        gvResults.ClearSelection();
        gvResults.FocusedRowHandle = RowHandle;
        gvResults.SelectRow(RowHandle);
        gvResults.MakeRowVisible(RowHandle);
        gvResults.Focus();
        IsItemSelecting = false;
    }

}   //End the mapControl1_MapItemClick() method

The IsItemSelecting property is a simple Boolean flag that I created and its use will be apparent in just a bit. The important take-away from the above snippet is that we want to have some sort of unique ID for each search result/map item so that we can find it within the GridControl and focus it. The flip-side to this is focusing a map item when a user clicks on a grid row. First, wire up a FocusedRowChanged event handler for the results GridView:

/// <summary>
/// Focused Row Changed event handler for the results GridView
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void gvResults_FocusedRowChanged(object sender, DevExpress.XtraGrid.Views.Base.FocusedRowChangedEventArgs e)
{
    if (IsItemSelecting == true)
        return;

    DataRow selectedResult = gvResults.GetFocusedDataRow();

    IsItemSelecting = true;

    SelectEquipmentIcon(selectedResult);

    IsItemSelecting = false;

}   //End the gvResults_FocusedRowChanged() method

In here we first check that IsItemSelecting flag. If a user clicks on an item from the map control, this event would be fired and vice versa resulting in an infinite loop. We want to make sure that don’t keep going back and forth like this, thus the need for that Boolean flag.

We’ll retrieve the DataRow object that corresponds to the selected row and pass this to the SelectEquipmentIcon method to zoom in on the truck that goes with the selected row:

/// <summary>
/// Selects the provided truck on the map
/// </summary>
/// <param name="selectedResult">A DataRow corresponding to the selected Grid row</param>
public void SelectEquipmentIcon(DataRow selectedResult)
{
    double SelectedLatitude = 0.0d;
    double SelectedLongitude = 0.0d;

    SelectedLatitude = Convert.ToDouble(selectedResult["OriginLatitude"]);
    SelectedLongitude = Convert.ToDouble(selectedResult["OriginLongitude"]);

    mapControl1.Zoom(1, true);
    mapControl1.CenterPoint = new GeoPoint(SelectedLatitude, SelectedLongitude);
    mapControl1.Zoom(10, true);

    VectorItemsLayer itemsLayer = mapControl1.Layers[1] as VectorItemsLayer;

    itemsLayer.SelectedItem = itemsLayer.GetMapItemBySourceObject(selectedResult);

}   //End the SelectEquipmentIcon class

And with that, we have a finished product! As always, the source code is provided with this post: XtraMapSearchResults

Visualizing data with the XtraMap control