//*************************************************************************
        //  Method: PopulateAreaWithImages()
        //
        /// <summary>
        /// Populates one area of the image column with images.
        /// </summary>
        ///
        /// <param name="oKeyColumnArea">
        /// Area from the key column.
        /// </param>
        ///
        /// <param name="sImageColumnName">
        /// Name of the image column.
        /// </param>
        ///
        /// <param name="oImageColumnArea">
        /// Corresponding area from the image column.
        /// </param>
        ///
        /// <param name="oImageSizePt">
        /// Size of each image, in points.
        /// </param>
        ///
        /// <param name="oOldImagesInColumn">
        /// A dictionary of zero or more key/value pairs.  The key is the
        /// Shape.Name of an old image in the image column and the value is the
        /// image, as a Shape.
        /// </param>
        ///
        /// <param name="oTemporaryImages">
        /// Contains information about the images that should be inserted.
        /// </param>
        ///
        /// <remarks>
        /// This method populates <paramref name="oImageColumnArea" /> with the
        /// temporary images specified by <paramref name="oTemporaryImages" />.
        /// </remarks>
        //*************************************************************************
        private static void PopulateAreaWithImages(
            Range oKeyColumnArea,
            Range oImageColumnArea,
            String sImageColumnName,
            SizeF oImageSizePt,
            Dictionary<String, Microsoft.Office.Interop.Excel.Shape>
            oOldImagesInColumn,
            TemporaryImages oTemporaryImages
            )
        {
            Debug.Assert(oKeyColumnArea != null);
            Debug.Assert(oImageColumnArea != null);
            Debug.Assert( !String.IsNullOrEmpty(sImageColumnName) );
            Debug.Assert(oOldImagesInColumn != null);
            Debug.Assert(oTemporaryImages != null);

            // Gather some required information.

            Int32 iRows = oKeyColumnArea.Rows.Count;

            Debug.Assert(iRows == oImageColumnArea.Rows.Count);

            Debug.Assert(oKeyColumnArea.Parent is Worksheet);

            Worksheet oWorksheet = (Worksheet)oKeyColumnArea.Parent;

            Microsoft.Office.Interop.Excel.Shapes oShapes = oWorksheet.Shapes;

            Object [,] aoKeyValues = ExcelUtil.GetRangeValues(oKeyColumnArea);

            Dictionary<String, String> oFileNames = oTemporaryImages.FileNames;

            // Set the row heights to fit the images.

            oKeyColumnArea.RowHeight = oImageSizePt.Height + 2 * ImageMarginPt;

            // Get the first cell in the image column.

            Range oImageCell = (Range)oImageColumnArea.Cells[1, 1];

            // Loop through the area's rows.

            for (Int32 iRow = 1; iRow <= iRows; iRow++)
            {
            String sKey, sFileName;

            // Check whether the row's key cell has a corresponding file name
            // in the dictionary.

            if (
                ExcelUtil.TryGetNonEmptyStringFromCell(aoKeyValues, iRow, 1,
                    out sKey)
                &&
                oFileNames.TryGetValue(sKey, out sFileName)
                )
            {
                // Give the picture a name that can be recognized by
                // GetImagesInColumn().

                String sPictureName = sImageColumnName + "-" + sKey;

                Microsoft.Office.Interop.Excel.Shape oPicture;

                // If an old version of the picture remains from a previous
                // call to this method, delete it.

                if ( oOldImagesInColumn.TryGetValue(sPictureName,
                    out oPicture) )
                {
                    oPicture.Delete();
                }

                String sFileNameWithPath = Path.Combine(
                    oTemporaryImages.Folder, sFileName);

                oPicture = oShapes.AddPicture(sFileNameWithPath,
                    MsoTriState.msoFalse, MsoTriState.msoCTrue,
                    (Single)(Double)oImageCell.Left + ImageMarginPt,
                    (Single)(Double)oImageCell.Top + ImageMarginPt,
                    oImageSizePt.Width,
                    oImageSizePt.Height
                    );

                oPicture.Name = sPictureName;
            }

            // Move down one cell in the image column.

            oImageCell = oImageCell.get_Offset(1, 0);
            }
        }
        //*************************************************************************
        //  Method: PopulateColumnWithImages()
        //
        /// <summary>
        /// Populates an image column in an Excel table (ListObject) with images
        /// that have been stored in a temporary folder.
        /// </summary>
        ///
        /// <param name="workbook">
        /// Workbook containing the table.
        /// </param>
        ///
        /// <param name="worksheetName">
        /// Worksheet containing the table.
        /// </param>
        ///
        /// <param name="tableName">
        /// Name of the table.
        /// </param>
        ///
        /// <param name="imageColumnName">
        /// Name of the image column.  The column gets added to the end of the
        /// table if it doesn't already exist.
        /// </param>
        ///
        /// <param name="keyColumnName">
        /// Name of the column containing the keys in the dictionary returned by
        /// <see cref="TemporaryImages.FileNames" />.  For each cell that contains
        /// a key in the dictionary, an image is inserted into the corresponding
        /// cell in the image column.
        /// </param>
        ///
        /// <param name="temporaryImages">
        /// Contains information about the images that should be inserted.
        /// </param>
        ///
        /// <remarks>
        /// If a column named <paramref name="imageColumnName" /> doesn't already
        /// exist, this method adds it to the end of the table.  It then populates
        /// the column with the temporary images specified by <paramref
        /// name="temporaryImages" />, and deletes the temporary folder containing
        /// the images.
        ///
        /// <para>
        /// The images are shown by default.  Call <see
        /// cref="ShowOrHideImagesInColumn" /> to hide or reshow them.
        /// </para>
        ///
        /// <para>
        /// If the specified table doesn't exist, this method does nothing.
        /// </para>
        ///
        /// </remarks>
        //*************************************************************************
        public static void PopulateColumnWithImages(
            Workbook workbook,
            String worksheetName,
            String tableName,
            String imageColumnName,
            String keyColumnName,
            TemporaryImages temporaryImages
            )
        {
            Debug.Assert(workbook != null);
            Debug.Assert( !String.IsNullOrEmpty(worksheetName) );
            Debug.Assert( !String.IsNullOrEmpty(tableName) );
            Debug.Assert( !String.IsNullOrEmpty(imageColumnName) );
            Debug.Assert( !String.IsNullOrEmpty(keyColumnName) );
            Debug.Assert(temporaryImages != null);

            ListObject oTable;
            Range oKeyColumnData;

            // Get the table and the key column data.

            if ( !ExcelUtil.TryGetTable(workbook, worksheetName, tableName,
                out oTable)
            ||
            !ExcelUtil.TryGetTableColumnData(oTable, keyColumnName,
                out oKeyColumnData)
            )
            {
            // Nothing can be done without the table or key column.

            return;
            }

            Range oImageColumnData;

            // Add the image column if it doesn't already exist.

            if ( !TryGetImageColumnData(oTable, imageColumnName,
            out oImageColumnData) )
            {
            // The image column doesn't exist and couldn't be added.

            return;
            }

            String sFolder = temporaryImages.Folder;

            if (sFolder == null)
            {
            // No temporary images were created, so nothing more needs to be
            // done.

            return;
            }

            // Reduce the key and image column data to visible areas only.

            Range oVisibleKeyColumnData, oVisibleImageColumnData;

            if (
            !ExcelUtil.TryGetVisibleRange(oKeyColumnData,
                out oVisibleKeyColumnData)
            ||
            !ExcelUtil.TryGetVisibleRange(oImageColumnData,
                out oVisibleImageColumnData)
            )
            {
            return;
            }

            Int32 iAreas = oVisibleKeyColumnData.Areas.Count;

            if (iAreas != oVisibleImageColumnData.Areas.Count)
            {
            return;
            }

            // Get the size of each image, in points.

            SizeF oImageSizePt =
            GetImageSizePt(temporaryImages.ImageSizePx, workbook);

            // Get any old images in the image column as a dictionary.  This
            // significantly speeds up the deletion of the old images, because
            // Excel doesn't have to do a linear search on Shape.Name as each image
            // is deleted by PopulateAreaWithImages().

            Debug.Assert(oTable.Parent is Worksheet);

            Dictionary<String, Microsoft.Office.Interop.Excel.Shape>
            oOldImagesInColumn = GetImagesInColumn( (Worksheet)oTable.Parent,
                imageColumnName );

            // Populate each area of the image column with images.

            workbook.Application.ScreenUpdating = false;

            try
            {
            for (Int32 iArea = 1; iArea <= iAreas; iArea++)
            {
                PopulateAreaWithImages(oVisibleKeyColumnData.Areas[iArea],
                    oVisibleImageColumnData.Areas[iArea], imageColumnName,
                    oImageSizePt, oOldImagesInColumn, temporaryImages);
            }
            }
            finally
            {
            workbook.Application.ScreenUpdating = true;
            }

            // Delete the entire temporary folder.

            try
            {
            Directory.Delete(sFolder, true);
            }
            catch (IOException)
            {
            // A user reported the following exception thrown from the above
            // Directory.Delete() call:
            //
            // "System.IO.IOException: The directory is not empty.:
            //
            // Others have reported this happenning at random times.  For
            // example:
            //
            // http://forums.asp.net/p/1114215/1722498.aspx
            //
            // I have also seen it happen from the command line outside of
            // .NET.  When it occurs, the directory IS empty but cannot be
            // accessed in any way.  The directory disappears when the machine
            // is rebooted.
            //
            // I can't figure out the cause or the fix.  Ignore the problem,
            // which seems to be benign.
            }
        }
        //*************************************************************************
        //  Method: CreateAndSaveSubgraphImages()
        //
        /// <summary>
        /// Creates images of a subgraph for one of a graph's vertices and saves
        /// the images to disk.
        /// </summary>
        ///
        /// <param name="oSubgraph">
        /// The subgraph to create images for.
        /// </param>
        ///
        /// <param name="sVertexName">
        /// Name of the vertex the subgraph is for.
        /// </param>
        ///
        /// <param name="oCreateSubgraphImagesAsyncArgs">
        /// Contains the arguments needed to asynchronously create subgraph images.
        /// </param>
        ///
        /// <param name="oThumbnailImages">
        /// Keeps track of the thumbnail images this method creates and stores in a
        /// temporary folder.
        /// </param>
        ///
        /// <remarks>
        /// This method creates zero, one, or two images of a subgraph and saves
        /// them to disk.
        /// </remarks>
        //*************************************************************************
        protected void CreateAndSaveSubgraphImages(
            IGraph oSubgraph,
            String sVertexName,
            CreateSubgraphImagesAsyncArgs oCreateSubgraphImagesAsyncArgs,
            TemporaryImages oThumbnailImages
            )
        {
            Debug.Assert(oSubgraph != null);
            Debug.Assert( !String.IsNullOrEmpty(sVertexName) );
            Debug.Assert(oCreateSubgraphImagesAsyncArgs != null);
            Debug.Assert(oThumbnailImages != null);
            AssertValid();

            if (oCreateSubgraphImagesAsyncArgs.SaveToFolder)
            {
            CreateAndSaveSubgraphImageInFolder(oSubgraph, sVertexName,
                oCreateSubgraphImagesAsyncArgs);
            }

            if (oCreateSubgraphImagesAsyncArgs.CreateThumbnails)
            {
            CreateAndSaveThumbnailImage(oSubgraph, sVertexName,
                oCreateSubgraphImagesAsyncArgs, oThumbnailImages);
            }
        }
        //*************************************************************************
        //  Method: CreateAndSaveThumbnailImage()
        //
        /// <summary>
        /// Creates a thumbnail image of a subgraph for one of a graph's vertices.
        /// </summary>
        ///
        /// <param name="oSubgraph">
        /// The subgraph to create an image for.
        /// </param>
        ///
        /// <param name="sVertexName">
        /// Name of the vertex the subgraph is for.
        /// </param>
        ///
        /// <param name="oCreateSubgraphImagesAsyncArgs">
        /// Contains the arguments needed to asynchronously create subgraph images.
        /// </param>
        ///
        /// <param name="oThumbnailImages">
        /// Keeps track of the thumbnail images this method creates and stores in a
        /// temporary folder.
        /// </param>
        //*************************************************************************
        protected void CreateAndSaveThumbnailImage(
            IGraph oSubgraph,
            String sVertexName,
            CreateSubgraphImagesAsyncArgs oCreateSubgraphImagesAsyncArgs,
            TemporaryImages oThumbnailImages
            )
        {
            Debug.Assert(oSubgraph != null);
            Debug.Assert( !String.IsNullOrEmpty(sVertexName) );
            Debug.Assert(oCreateSubgraphImagesAsyncArgs != null);
            Debug.Assert(oCreateSubgraphImagesAsyncArgs.CreateThumbnails);
            Debug.Assert(oThumbnailImages != null);
            AssertValid();

            if (oThumbnailImages.Folder == null)
            {
            // Create a temporary folder where the thumbnail images will be
            // stored.

            String sTemporaryFolder = Path.Combine(
                Path.GetTempPath(),
                Path.GetRandomFileName()
                );

            Directory.CreateDirectory(sTemporaryFolder);

            oThumbnailImages.Folder = sTemporaryFolder;
            }

            // Save the graph to a bitmap.

            Bitmap oBitmap = CreateSubgraphImage(oSubgraph,
            oCreateSubgraphImagesAsyncArgs,
            oCreateSubgraphImagesAsyncArgs.ThumbnailSizePx);

            try
            {
            // Save the bitmap in the temporary folder.

            String sTemporaryFileName = SaveSubgraphImage(oBitmap,
                oThumbnailImages.Folder, sVertexName,
                oCreateSubgraphImagesAsyncArgs
                );

            // Add the file name to the dictionary.  They key is the vertex
            // name and the value is the file name, without a path.

            oThumbnailImages.FileNames[sVertexName] = sTemporaryFileName;
            }
            finally
            {
            GraphicsUtil.DisposeBitmap(ref oBitmap);
            }
        }
        //*************************************************************************
        //  Method: CreateSubgraphImagesInternal()
        //
        /// <summary>
        /// Creates an image of a subgraph for each of a graph's vertices and saves
        /// the images to disk.
        /// </summary>
        ///
        /// <param name="oCreateSubgraphImagesAsyncArgs">
        /// Contains the arguments needed to asynchronously create subgraph images.
        /// </param>
        ///
        /// <param name="oBackgroundWorker">
        /// A BackgroundWorker object.
        /// </param>
        ///
        /// <param name="oDoWorkEventArgs">
        /// A DoWorkEventArgs object.
        /// </param>
        //*************************************************************************
        protected void CreateSubgraphImagesInternal(
            CreateSubgraphImagesAsyncArgs oCreateSubgraphImagesAsyncArgs,
            BackgroundWorker oBackgroundWorker,
            DoWorkEventArgs oDoWorkEventArgs
            )
        {
            Debug.Assert(oCreateSubgraphImagesAsyncArgs != null);
            Debug.Assert(oBackgroundWorker != null);
            Debug.Assert(oDoWorkEventArgs != null);
            AssertValid();

            // Create an object to keep track of the thumbnail images this method
            // creates and stores in a temporary folder.

            TemporaryImages oThumbnailImages = new TemporaryImages();

            oThumbnailImages.ImageSizePx =
            oCreateSubgraphImagesAsyncArgs.ThumbnailSizePx;

            oDoWorkEventArgs.Result = oThumbnailImages;

            ICollection<IVertex> oVertices;

            if (oCreateSubgraphImagesAsyncArgs.SelectedVerticesOnly)
            {
            oVertices = oCreateSubgraphImagesAsyncArgs.SelectedVertices;
            }
            else
            {
            oVertices = oCreateSubgraphImagesAsyncArgs.Graph.Vertices;
            }

            Int32 iSubgraphsCreated = 0;

            Boolean bSaveToFolder = oCreateSubgraphImagesAsyncArgs.SaveToFolder;

            Boolean bCreateThumbnails =
            oCreateSubgraphImagesAsyncArgs.CreateThumbnails;

            if (bSaveToFolder || bCreateThumbnails)
            {
            foreach (IVertex oVertex in oVertices)
            {
                if (oBackgroundWorker.CancellationPending)
                {
                    if (oThumbnailImages.Folder != null)
                    {
                        // Delete the entire temporary folder.

                        Directory.Delete(oThumbnailImages.Folder, true);

                        oThumbnailImages.Folder = null;
                    }

                    oDoWorkEventArgs.Cancel = true;
                    break;
                }

                String sVertexName = oVertex.Name;

                oBackgroundWorker.ReportProgress(0,
                    String.Format(
                        "Creating subgraph image for \"{0}\"."
                        ,
                        sVertexName
                    ) );

                // Create a subgraph for the vertex.

                IGraph oSubgraph = CreateSubgraph(oVertex,
                    oCreateSubgraphImagesAsyncArgs);

                // Create and save images for the subgraph.

                CreateAndSaveSubgraphImages(oSubgraph, sVertexName,
                    oCreateSubgraphImagesAsyncArgs, oThumbnailImages);

                iSubgraphsCreated++;
            }
            }

            oBackgroundWorker.ReportProgress(0,
            String.Format(
                "Done.  Created {0} subgraph {1}."
                ,
                iSubgraphsCreated.ToString(ExcelTemplateForm.Int32Format),
                StringUtil.MakePlural("image", iSubgraphsCreated)
                ) );
        }