Converting a COM IPicture into a .NET Image 3

This article was originally published on the Mapping-Tools Howto pages for programming MapPoint. Although it was originally written with MapPoint in mind, it is useful in a wider context where a .NET application is trying to use a COM image (IPicture). Hence it has been retained and moved here.

A program can create MapPoint images (bitmaps) in a number of ways. Common approaches are to copy a map image to the clipboard, or to save the map as a web page and to extract the resulting GIF image. These are both messy approaches. A third approach is often ignored by .NET programmers, and that is to use the GetPictureFromObject() method. This method creates a Visual Basic (VB6) Picture object which is different to the .NET Image object. This article shows you how to use GetPictureFromObject() in a C# program, and to convert the Picture into a form that can be used by .NET, and to save it as a PNG bitmap file.

Converting a Picture to an Image

There are a number of approaches to converting a VB6 Picture to a .NET Image. Some rely on out-dated compatibility libraries. Virtually all assume the Picture is a bitmap. The reality is that both the Picture and Image classes can encapsulate different image types, and GetPictureFromObject() returns the image as metafile and not a pure bitmap. Windows Metafile (WMF) is a vector format that dates back to the early 1990s, but continues to be supported by .NET. It can also include bitmaps and icons.

A VB6 Picture is actually a COM object that implements the IPictureDisp interface. Knowing this, we can use the IPictureDisp interface to query the Picture object, and to extract the internal metafile.

The following implementation is based on the second approach in Andrew Whitechapel’s article “Converting between IPictureDisp and System.Drawing Image”. This only worked with pure bitmaps, but I have amended the PictureDispToImage() method to also handle metafiles. I have also removed the (reverse) Image to IPictureDisp conversion because this is not required.

The conversion code has been implemented as a method in a static conversion class. Before compiling it, you will need to add a reference to the COM library stdole. This library includes the definition of the IPictureDisp interface. Here is the conversion code:

// Converts IPictureDisp bitmaps and metafiles to Drawing.Image objects
// Requires additional references to System.Drawing (.NET) and stdole (COM).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Drawing;
using System.Drawing.Imaging;
using System.Text;
using System.Runtime.InteropServices;

namespace PictureBoxDemo
{
    // Class of static members which provide conversion methods
    public class OleCreateConverter
    {
        // PICTYPE values for the IPictureDisp Type property
        const short _PictureTypeBitmap = 1;
        const short _PictureTypeMetafile = 2;

        // Return a Drawing.Image object representing the supplied IPictureDisp
        // Returns null if it cannot be converted or if the type is not a Bitmap or a Metafile
        public static Image PictureDispToImage(stdole.IPictureDisp pictureDisp)
        {
            Image image = null;
            if (pictureDisp == null)
                return image; // no image to convert
            if (pictureDisp.Type == _PictureTypeBitmap)
            {
                IntPtr paletteHandle = new IntPtr(pictureDisp.hPal);
                IntPtr bitmapHandle = new IntPtr(pictureDisp.Handle);
                image = Image.FromHbitmap(bitmapHandle, paletteHandle);
            }
            else if (pictureDisp.Type == _PictureTypeMetafile)
            {
                IntPtr metafileHandle = new IntPtr(pictureDisp.Handle);
                image = new Metafile(metafileHandle, new WmfPlaceableFileHeader());
            }
            return image;
        }

    }
}

 

Calling GetPictureFromObject

Next we need a demonstration program that calls GetPictureFromObject() and uses the above class to convert the image into a usable form. The following code is a simple form application that includes a PictureBox control and a “Go” button. Pressing “Go” starts MapPoint in the background, draws something interesting (pushpins for the various venues in London’s Olympic Park), creates an image, displays it in the PictureBox and saves it as a PNG to the desktop.

For simplicity, I have not included the GUI definition. The PictureBox is named ‘myPictureBox’ and the ‘Go’ button has a click handler called ‘buttonGo_click()’. You should include references to stdole and System.Drawing (as above), and the MapPoint COM library. This particular implementation was built with MapPoint 2011 Europe but it also compiles with MapPoint 2013 North America – unfortunately MapPoint 2013 Europe had yet to be released when this article was written.

The namespace references are as above, plus the addition of the MapPoint namespace:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using stdole;
using MapPoint;

We need to perform some unit conversions, so we define a static constant:

private const double Inch2HIMETRIC = 25.4 * 100.0; // HIMETRIC units per Inch

GetPictureFromObject() returns an object defined in HIMETRIC units. This is a VB6-era device independent unit that is similar to a twip, but has no direct support in .NET’s Drawing library. There are 100 HIMETRIC units in a millimetre. Hence Inch2HIMETRIC contains the number of HIMETRIC units in an inch.

Here is the definition for buttonGo_Click():

private void buttonGo_Click(object sender, EventArgs e)
{
    // First we start MapPoint in the background
    MapPoint._Application myApp = new MapPoint.Application();
    MapPoint.Map myMap = myApp.ActiveMap;
    myApp.Visible = false;

    // Make the map more interesting by adding pushpins for each of the venues in the 
    // London 2012 Olympic Park. These pushpin symbols should work with MapPoint 2010,2011, and 2013
    MapPoint.Pushpin pin = myMap.AddPushpin(myMap.GetLocation(51.538611, -0.016389, 1000.0), "Olympic Stadium");
    pin.Symbol = 84;
    pin.BalloonState = MapPoint.GeoBalloonState.geoDisplayName;
    pin = myMap.AddPushpin(myMap.GetLocation(51.54153, -0.01292, 1000.0), "Water Polo");
    pin.Symbol = 116;
    pin.BalloonState = MapPoint.GeoBalloonState.geoDisplayName;
    pin = myMap.AddPushpin(myMap.GetLocation(51.5486, -0.0139, 1000.0), "Basketball Arena");
    pin.Symbol = 210;
    pin.BalloonState = MapPoint.GeoBalloonState.geoDisplayName;
    pin = myMap.AddPushpin(myMap.GetLocation(51.5402, -0.0106, 1000.0), "London Aquatics Centre");
    pin.Symbol = 323;
    pin.BalloonState = MapPoint.GeoBalloonState.geoDisplayName;
    pin = myMap.AddPushpin(myMap.GetLocation(51.55039,-0.015171, 1000.0), "Velopark");
    pin.Symbol = 176;
    pin.BalloonState = MapPoint.GeoBalloonState.geoDisplayName;
    pin = myMap.AddPushpin(myMap.GetLocation(51.544393, -0.020042, 1000.0), "Copper Box");
    pin.Symbol = 42;
    pin.BalloonState = MapPoint.GeoBalloonState.geoDisplayName;
    pin = myMap.AddPushpin(myMap.GetLocation(51.54944, -0.02056, 1000.0), "Riverbank Arena");
    pin.Symbol = 306;
    pin.BalloonState = MapPoint.GeoBalloonState.geoDisplayName;
    pin = null;

    // Switch the left pane (legend, search, etc) off, and zoom in to the Stratford / Lea Valley area
    myApp.PaneState = MapPoint.GeoPaneState.geoPaneNone;
    myMap.Location = myMap.GetLocation(51.54433, -0.01633, 5.0);

    // Find the size of the PictureBox in pixels
    // Then convert it into HIMETRIC units for MapPoint
    // We perform the conversion using Inch2HIMETRIC (see above) and 
    // the system DPI values
    // Note: MapPoint/VB6's definition of "long" is the same as C#'s definition for "int".
    int iWidth,iHeight;
    using (Graphics g = myPictureBox.CreateGraphics())
    {
        iWidth = (int) ( (double)myPictureBox.Width * Inch2HIMETRIC / g.DpiX );
        iHeight = (int) ( (double)myPictureBox.Height * Inch2HIMETRIC / g.DpiY );
    }

    // GetPictureFromObject() is defined as a member of the MapPointUtilities class
    MapPoint.MapPointUtilities myMapUtils = new MapPoint.MapPointUtilities();

    // Create the Picture for the current map, as an stdole.IPictureDisp COM object
    stdole.IPictureDisp ipicMap = (stdole.IPictureDisp) myMapUtils.GetPictureFromObject(myMap, iWidth, iHeight);

    // Convert it to a (metafile) Drawing.Image using the OleCreateConverter defined above
    System.Drawing.Image myImage = OleCreateConverter.PictureDispToImage( ipicMap);

    // Copy the Drawing.Image to the Picture box
    // Refresh and stretch for good measure
    myPictureBox.Image = myImage;
    myPictureBox.SizeMode = PictureBoxSizeMode.StretchImage; 
    myPictureBox.Refresh();

    // Save it as a PNG
    // Although a GIF is capable of holding a MapPoint map, PNGs are generally prefered 
    myImage.Save(@"c:\Users\richard\Desktop\london2012.png", System.Drawing.Imaging.ImageFormat.Png);

    // Clean MapPoint up
    myMap.Saved = true;
    myMap = null;
    myApp.Quit();
    myApp = null;

}

And that is it! Here is the final results of the form and PictureBox:

The PictureBox example

The PictureBox example

Converting Picture objects into .NET Drawing.Image objects can be messy due to the lack of any good casts or conversion functions. Also online examples invariably concentrate on pure bitmaps and not the metafiles used by MapPoint. However, as you can see, conversion is not too difficult, and the end result is much easier and cleaner than using the clipboard or the “Save to Web Page” function.

3 thoughts on “Converting a COM IPicture into a .NET Image

  1. Reply Joe Colon Apr 7,2013 12:36 pm

    Hello, this is an amazing article. Is there anyway to do this from ASP.Net? I tried to run the code from a web page but it fails when it reaches the stdole line. Probably an out of process permission issue? It would be great to get this working from ASP.Net.

    • Reply Richard Marsden Apr 8,2013 7:05 am

      It might be that stdole.dll isn’t installed, or it hasn’t been installed into the GAC properly? It could be permissions – I am far from being an ASP.NET expert.

      Note that using MapPoint in a web server environment is generally not recommended, for the same reasons as Word and Excel are not recommended. This is because they are desktop user applications, and they are difficult to manage in what is essentially a headless server environment. The user can’t respond to error messages, life cycle management is ‘dirty’, etc.

  2. Reply Todd Aug 21,2015 2:11 pm

    Excellent and concise article. I got this working with Windows 7, VS 2011 and MapPoint 2013 NA. I believe this would work with VS 2013 no problem.

    I chose a C# Windows Forms Application and then simple used the ToolBox to draw a picture box into the default form and renamed it myPictureBox. Then I added a button and included the supplied code into its method.

    The references by default here:

    C:\Program Files (x86)\Microsoft.NET\Primary Interop Assemblies\stdole.dll
    C:\Program Files (x86)\Microsoft MapPoint 2013\MPNA83.tlb

Leave a Reply