In a previous post I discussed several way to access the thumbnail image that’s associated with each document. One of these ways is to use the ThumbnailView component. I received a question recently regarding the ThumbnailView component not working as expected. After quite a bit of digging and some testing I believe I know what the problem is and how to fix it.
I believe this an issue only if you’re using .Net (VB or C#) and are using 64-bit Windows. I don’t think there are problems with any other configurations but if there are, please let me know.
COM and .Net on a 64-Bit OS Overview
Before I describe the solution, I think it might be worth taking a minute to describe what’s happening when you use a COM component (the thumbnail view component in this case) from .Net program on a 64-bit system. The picture below attempts to show all of the various pieces that need to play together to get this work. First, is the COM component itself. The thumbnail viewer was created using C++ and the resulting dll is compiled for either 32 or 64-bit. On a 32-bit system you can only use 32-bit components. On a 64-bit system you can use either 32 or 64-bit components because 64-bit windows supports an emulator that is able to run 32-bit applications. COM dll components run within the process of the program using it and a 32-bit component can only run within a 32-bit process and a 64-bit component can only run within a 64-bit process.
The registry is used because COM components can be located anywhere on a computer but other programs need to be able to know they’re available and where they’re located. The components make themselves known by adding information to the registry. There are separate areas in the registry where 32-bit and 64-bit components register themselves.
Another piece that makes the 32-bit vs. 64-bit issue described above even more confusing is that when writing a .Net program you can specify that the runtime will be compatible with 32-bit (x86), 64-bit (x64), or “Any CPU” (both 32 and 64-bit). The “Any CPU” option is the interesting, and confusing, option. It’s the option I almost always use. The reason that .Net can support the “Any CPU” option is that when you compile your program, .Net compiles your program into MSIL (Microsoft Intermediate Language) which is a CPU independent set of instructions. When your program is run, it is converted by the JIT (Just-In-Time) compiler into native code. This means that even if your program was compiled for “Any CPU”, when it runs it will actually be running as a native 32-bit or 64-bit application.
Because the COM component runs in-process to your application it must match the bitness of the running application. For example, if you compile your application for “Any CPU” and run your program on a 64-bit machine, the .Net Framework will run it as a true 64-bit application and the 64-bit version of the thumbnail viewer will be required. If you compile your application for 32-bit and run it on a 64-bit machine, it will run as a 32-bit app and will require the 32-bit version of the thumbnail viewer. If the correct version of the COM component isn’t available, it will fail to run.
In addition to your program and the COM component, there is one other piece needed when using a COM component with .Net; an Interop assembly. Your .Net program can’t directly call a COM component. To provide support for COM, .Net uses the concept of Interop assemblies which are .Net components that provide a .Net interface to a COM component. When you use the COM component, your program is actually calling functions in the interop assembly, which translates those calls and makes the equivalent call in the COM component. The Interop serves as a wrapper for the COM component making it compatible with .Net. These interop assemblies are dll’s and can also be created for 64-bit, 32-bit, or agnostic (equivalent to “Any CPU”). But like the component, it must also match the bitness of the program it will be running within.
In summary, there are three runtime pieces; the COM component, the .Net Interop, and your program. If you’re running on a 32-bit system, all of them must be 32-bit. If you’re running on a 64-bit system you can run either 32-bit or 64-bit but all of the pieces must be 32-bit or 64-bit. You can’t mix and match the bitness.
The Problem
When programming with .Net and using a COM component, it’s somewhat transparent that there is an interop assembly involved. When you reference the COM component into your project, Visual Studio automatically creates the interop behind the scenes. The problem is that it’s creating a 32-bit compatible interop. If you’re creating a 32-bit application, then everything is fine, but for a 64-bit application it’s not going to work.
I found the problem by doing some research on the web and discovering this article on a Microsoft support site that gave me the clue I needed. It also indicates that this is fixed in VS 11.
The Fix
Here’s the fix that I’m using to get this to work. You need to manually create the interop for the thumbnail viewer component. By manually creating it, you can specify the machine type that it’s compatible with. In this case either “x64” or “Agnostic” will work because at runtime on an x64 machine they will both result in true X64 machine code.
TlbImp.exe InventorThumbnailView.dll
/machine:Agnostic
/namespace:InventorThumbnailViewLib
/out:Interop.InventorThumbnailView.dll
Here’s a link to a text file that has the complete command line, with the full paths, so you can just copy and past this into a DOS command window. You will need to edit the path for the output interop dll to where your project is.
Once you’ve created the interop, Use the References command in Visual Studio to reference it into your project. Use the Browse tab on the References dialog so you can browse and select the interop dll. You’ll reference this instead of the InventorThumbNailView.dll file. Now it should all work.
Below is some code from a very simple Windows Forms project that I created. After creating the new project, I added a picture box control (with the SizeMode property set to “StretchImage”) and a button to the dialog. I also added an open file dialog. Finally, I added a reference to the .Net component “stdole”. Below is the code that’s contained within the Click event of the button. It uses the file dialog to get the filename of an Inventor file, uses the thumbnail view component to get the thumbnail from the Inventor file and then displays it. The component returns the image as an IPictureDisp object. Next, it converts this object to a .Net Image object, which it finally assigns to the picture box. See below for some more discussion about this conversion process.
' Code in the button Click event.
With OpenFileDialog1
.Filter = "Inventor Files (*.ipt;*.iam;*.idw)|*.ipt;*.iam;*.idw"
If .ShowDialog() = System.Windows.Forms.DialogResult.OK Then
' Create an instance of the thumbnail viewer component.
Dim thumbviewer As New _
InventorThumbnailViewLib.ThumbnailProvider
' Use the component to get the thumbnail.
Dim pic As stdole.IPictureDisp = Nothing
Dim handle As Long = 0
' Use workaround to make sure handle is a positive number.
' Otherwise the call to PictureDispToImage will fail.
Do
pic = thumbviewer.GetThumbnail(.FileName)
handle = pic.Handle
Loop While handle < 0
' Convert the IPictureDisp into an Image to
' display it in the picture box.
Dim img As Image = AxHostConverter.PictureDispToImage(pic)
PictureBox1.Image = img
End If
End With
The object that represents a bitmap image in COM is an IPictureDisp object, (which is defined in the stdole library). .Net represents a bitmap image using the Image object. This means there needs to be a conversion from an IPictureDisp objet to an Image object to be able to use the returned bitmap in .Net. There is a Visual Basic compatibility library that contains routines to do this. Unfortunately, if you use them with .Net 4, you’ll get a warning message that these functions are obsolete and are only supported on 32-bit. You can read more here: http://go.microsoft.com/fwlink/?linkid=160862.
To avoid these warnings and possible problems in the future you’ll need to use something else. Unfortunately, Microsoft hasn’t provided a simple alternative. Below is a class that exposes some functions to do this conversion from one type to the other. I wraps some internal functionality on another Microsoft class to it can be used in a general way. To use it in your project, just copy the class below into your project. The PictureDispToImage function defined in this AxHostConverter class is used in the code above.
There’s also some funny code in the example above that’s a workaround to a problem I found while testing. When testing, it would periodically fail to do the conversion. The failure seemed random with a “A generic error occurred in GDI+” failure. I accidentally discovered that the failure was occurring whenever the Windows handle being provided by the IPictureDisp object was negative. The workaround I put in the code above keeps getting an new IPictureDisp until the handle is a positive number. I stress tested this through several thousand iterations and the most it ever looped to get a positive number was 10, so I think the chance of this being an endless loop is too small to worry about. About 85% the first return is positive, another 10% took two calls, and the remaining 5% is 3 or more calls. I believe the cause of this problem is that the number is actually an unsigned int, but it’s being treated as a regular int which results in a negative number for large numbers. Anyway, this seems to work around the problem.
' Utility class that provides support for converting between
' IPictureDisp and Image objects.
Friend Class AxHostConverter
Inherits AxHost
Private Sub New()
MyBase.New("")
End Sub
Public Shared Function ImageToPictureDisp( _
ByVal objImage As Image) As stdole.IPictureDisp
Return DirectCast(GetIPictureDispFromPicture(objImage), _
stdole.IPictureDisp)
End Function
Public Shared Function PictureDispToImage( _
ByVal pictureDisp As stdole.IPictureDisp) As Image
Dim objPicture As System.Drawing.Image
objPicture = CType(GetPictureFromIPicture(pictureDisp), _
System.Drawing.Image)
Return objPicture
End Function
End Class