|Important||This document may not represent best practices for current development, links to downloads and other resources may no longer be valid. Current recommended version can be found here.|
Working with Word 2003 Images Programmatically
This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.
Summary: Microsoft Office Word 2003 is not a tool for text only. It enables you to use programmatically editable images such as shapes, charts, and so on. You can save bitmap (BMP) versions of them to your hard drive, create captions, and do many other things to them with Word's object model. (7 printed pages)
Mark Iverson, Microsoft Corporation
Applies to: Microsoft Office Word 2003
Have you ever had a document with ten, twenty, or even 100 images, and then as an afterthought decided you wanted to add a caption to each of those images? This is an easy task through the user interface (UI). All you have to do is select each image, one at a time, and choose Insert, then Reference, then Caption. Unfortunately doing this through an image-heavy document is a boring chore and takes up a lot of time.
Fortunately, the Word object model lets you to apply a caption to a selected object. An add-in that does this for you is only a small chunk of code away. The sample add-in accompanying this article demonstrates how to do this. It also demonstrates many other details about working with shapes and images in Word programmatically that are useful to know about.
To create the add-in framework, I used the excellent Knowledge Base article—How to Build an Office COM Add-in by Using Visual Basic .NET to build a blank add-in containing a button in the standard toolbar in Word. From there, I added a simple ListView control modified to contain CheckBox objects and an icon with a small thumbnail for each of its items. Each item is populated with one shape from a Word document with a small icon of it appearing next to the text that describes it as either inline or not, along with its index. To implement these modifications to the ListView control, set the CheckBoxes property to True and the View property to Details. Setting the View property is what displays icons with each item. For example, see the following:
Me.ImageCheckedListBox.CheckBoxes = True Me.ImageCheckedListBox.View = System.Windows.Forms.View.Details
The icons displayed in the ListView are stored in an ImageList control, which is populated with the icons while iterating through all the images in the document. Since we want the ListView populated when the add-in dialog box first displays, the function that does the icon work, PutImagesInChecklist, is called from the New function of the form.
The range in the New function that is associated with the selection in Word is stored in the global variable rangeStart. The add-in uses selection objects to select and deselect items, so we want to restore the application's selection state when the add-in closes. To do this, simply call rangeStart.Select().
Let us take a closer look at how the PutImagesInChecklist routine works. As a safety precaution, both the ListView and the ImageList are checked to see if either contains any items. If so, they are removed to ensure the add-in begins with them empty. The For...Eachclause accomplishes this.
Next, the add-in must check for all of the two shape types in Word documents, which are:
Shape Objects. A Shape object represents an object in the drawing layer. A Shape object is anchored to a range of text but is free-floating and can be positioned anywhere on the page. Examples include AutoShapes, pictures, Microsoft ActiveX controls, OLE objects, or freeform objects. A Shape object is part of the Shapes collection of the Document object.
InlineShape Objects. An InlineShape object represents an object in the text layer of a document and must be a picture, an OLE object, or an ActiveX control. An InlineShape object is a member of an InlineShapes collection within a Document, Range, or Selection object. Note that unlike a Shape object, the InlineShape object does not have a name associated with it, so, aside from iterating through a collection, the only way to access a specific InlineShape object is by its index.
An overloaded function, called the AddInlineShapeToImageList function, does the work of creating a bitmap from a shape and then adding it to the ImageList object. One overloaded version of the function takes an InlineShape object, the other takes a Shape object. Both are passed by value because we do not want to modify either.
The algorithm to create a bitmap from an InlineShape is straightforward:
Select the shape.
Copy the selected shape to the clipboard.
Get the clipboard contents using an IDataObject.
Using the IDateObject, call GetDataPresent with System.Drawing.Bitmap as a parameter to verify the clipboard contains data that can be converted to a bitmap.
If GetDataPresent returns true, then it is safe to call GetData to create the bitmap and then add it to the ImageList.
This is what the function looks like in the add-in:
Private Sub AddInlineShapeToImageList(ByRef inlineShape As Word.Shape, _ Word.InlineShape) ' This function creates a bitmap of an inline shape and then ' adds it to the imagelist. ' First select and copy the shape. inlineShape.Select() wordApp.Selection.CopyAsPicture() ' Next get the data from the clipboard with an IDataObject. Dim data As IDataObject data = Clipboard.GetDataObject() If data.GetDataPresent(GetType(System.Drawing.Bitmap)) Then ' Create a bitmap from the data in the IDataObject. Dim bmp As Bitmap bmp = CType(data.GetData _ (GetType(System.Drawing.Bitmap)), Bitmap) ' Add the bitmap to the ImageList. The ImageList contains ' the bitmaps displayed in the ImageCheckedListBox. ImageList.Images.Add(bmp) End If End Sub
Getting the bitmap of a Shape object is slightly less intuitive, but still easy to do. Note that metafiles from Shape objects that Microsoft .NET–based applications add to the Clipboard are not visible to other applications. The solution is to use the CF_ENHMETAFILE format. However, limitations in the current implementation of the Microsoft .NET Framework do not allow this, so you must use the Microsoft Win32 Clipboard application programming interfaces (API). This is discussed in detail in the Knowledge Base Article - Metafiles on Clipboard Are Not Visible to All Applications. The result of implementing the Win32 Clipboard API is the ClipboardAPI class in the add-in, which is identical to the code in the knowledge base article.
The AddInlineShapeToImageList function for Shape objects contains a call to a helper function, called GetShapeBitmap. This function performs the majority of the work. It selects the shape and copies it to the clipboard. Next, it uses an IntPtr, which is a platform-specific representation of a pointer or handle, to store the clipboard contents. The clipboard contents are retrieved as an enhanced metafile using ClipboardAPI.GetClipboardData(). The function appears as follows:
Private Sub GetShapeBitmap(ByRef shape As Word.Shape, _ ByRef bmp As Bitmap) 'Copy the shape to the clipboard. shape.Select() wordApp.Selection.CopyAsPicture() 'Declare an IntPtr, which is a platform-specific type that is 'used to represent a pointer or a handle. Dim ip As IntPtr Dim metaFile As System.Drawing.Imaging.Metafile 'Declare a boolean to test the success of function calls. Dim bRet As Boolean bRet = ClipboardAPI.OpenClipboard(Me.Handle) If bRet = True Then 'Verify the clipboard contains data available 'as an enhanced metafile. bRet = _ ClipboardAPI.IsClipboardFormatAvailable(CF_ENHMETAFILE) <> 0 End If If bRet = True Then 'Store the clipboard's contents in the IntPtr. ip = ClipboardAPI.GetClipboardData(CF_ENHMETAFILE) End If 'Verify the IntPrt contains data before proceeding. Passing 'an empty IntPtr to System.Drawing.Imaging.Metafile results 'in an exception. If Not IntPtr.Zero.Equals(ip) Then metaFile = New System.Drawing.Imaging.Metafile(ip, True) ClipboardAPI.CloseClipboard() Dim image As System.Drawing.Image = metaFile Me.MyImage.Image() = metaFile bmp = New Bitmap(image, 20, 20) End If End Sub
The bitmap created by the GetShapeBitmap method is passed back to the caller through the bmp parameter. Once the AddInlineShapeToImageList function has the bitmap, it inserts it into the ImageList collection.
The InlineShapes objects are iterated through and added to the ImageList collection first, followed by the Shape objects. Therefore, the ListView object displays the InlineShape objects in the order of their indexes followed by Shape objects in their index order.
Once you have a bitmap, you can then save it to your hard disk with the Save function of the Bitmap class. For example, bmp.Save("c:\mybitmap.bmp").
The number of shapes in a collection along with indexes can be used to iterate through the shapes, as follows:
Dim index As Integer For index = 1 To doc.InlineShapes.Count 'Get bitmap from the shape and add it to 'the ImageList here. Next
However, I chose to use the IEnumerator class to iterate through both the Shape objects and the InlineShapes collection. The IEnumerator interface is the base interface for all enumerators. A large benefit from using IEnumerator, aside from arguably cleaner code, is that it is safer because it only allows reading a collection's data and no modification. Notice that initially the enumerator is positioned before the collection's first element, so you must call the MoveNext method to advance it to the first element before you begin reading the collection's data.
Now that we have all the shapes easily identifiable in the ListControl object, let's add some captions to them. You can check the shapes to which you want to add a numbered caption and then click the Add Captions button. This takes you into the section of code called Caption_Click where a For Each...Next loop goes through each item in the ListView control and adds a caption to the checked shapes.
Notice that a checked ListView item's text is evaluated to see if it begins with "Inline" rather than "Non-inline" to see what type of shape it is. This indicates the collection in which to look for the shape. The index of the shape in its respective collection is taken from the text by looking at the characters that occur after the "#" character. Note that this type of identification is very simple and useful only for English. Should something like this need to be localized, then all hard-coded strings, such as "Inline," must be removed entirely from the code and put into string tables. Localizers can later translate without affecting the code.
The shape to add a caption to is found in SelectInlineShape and SelectShape, which only differ according to the collection that is used. The former function is here:
Private Sub SelectInlineShape(ByVal index As Integer, _ ByRef buffer As Integer) index += buffer Try Dim inlineShape As Word.InlineShape = wordApp.ActiveDocument.InlineShapes(index) inlineShape.Select() Catch ex As Exception buffer += 1 'To prevent an infinite loop in an unusual circumstance, 'verify that the buffer variable is less than a value 'that should never be reached. If that value is reached, 'then display the error and leave the function. Dim unexpectedBuffer As Integer = 100 If (buffer < unexpectedBuffer) Then SelectInlineShape(index, buffer) Else MsgBox(ex.Message) End If End Try End Sub
You may be a bit perplexed by the buffer parameter. An anomaly in the Word InlineShape collections affects their indexes. If an inline shape is preceded by a non-inline shape, then the indexes in the collection may be bumped up by one. For example, if the first shape in a document is a regular shape, and the next shape is an InlineShape, then that inline shape has an index of two, not one. The buffer parameter in the code example accounts for this issue. If you attempt to get the shape in our example by passing "1" as the index, you may receive an error that the item does not exist in the collection. Therefore, we can use a Try...Catch block to isolate these rare cases. If an error occurs, it is caught by the Try...Catch block. The Try...Catch block adds one to the buffer and then recursively calls the function one more time. Notice that the function adds the buffer to the index, so if index 3 results in an "item does not exist" error, it automatically tries again with the original index plus the buffer. The first time the function is called, the buffer in initialized to zero, so if the SelectInlineShape function is called recursively, the buffer is incremented to one for the first recursion and that is added to the index. If a second recursion is necessary, the buffer is bumped to two, and so on.
Once the shape in question is found, it is selected and then the following line is called on the selection, adding the caption.
' Add a caption to the selection. wordApp.Selection.InsertCaption(Label:="Figure", TitleAutoText:="", _ Title:="", Position:= _ Word.WdCaptionPosition.wdCaptionPositionBelow, ExcludeLabel:=0)
The parameters used in this instance of InsertCaption create captions that label each shape sequentially. For example, "Figure 1," Figure 2," and so on.
Working with shapes in Word is simpler than it may initially appear. You easily can iterate through both inline and non-inline shapes and then customize them however you want. You can create bitmaps of them and then turn those bitmaps into icons or save them to your hard disk. You can add captions to them all at once. You can also do many other things with them—just take a peak at the properties you can change on shapes and inline shapes to see what more you can do.
The following is a list of additional resources that can help you in working with Word 2003 images programmatically:
Shape Object (http://msdn.microsoft.com/library/en-us/off2000/html/woobjShape.asp)
InlineShape Object (http://msdn.microsoft.com/library/en-us/off2000/html/woobjinlineshape.asp)