Export (0) Print
Expand All
Debugging: Root Out Elusive Production Bugs with These Effective Techniques
Smart Tags: Simplify UI Development with Custom Designer Actions in Visual Studio
Ten Essential Tools: Visual Studio Add-Ins Every Developer Should Download Now
XML Comments: Document Your Code in No Time At All with Macros in Visual Studio
Expand Minimize

Using P/Invoke to Call Unmanaged APIs from Your Managed Classes

Visual Studio .NET 2003
 

Microsoft Corporation

March 2006

Summary: Learn how to use P/Invoke to call unmanaged Win32 APIs from managed code. These sample applications show how to mute sounds, change Windows resolution, and display balloon tips from your managed code. (33 printed pages)

Click here to download pinvokesamples.msi.


Contents

Introduction
What is P/Invoke?
Background Information
Conclusion

Introduction

The .NET Framework Base Class Library (BCL) consists of thousands of classes covering almost all of the functionality required to create powerful applications. Certain functionality,—such as changing the windows resolution or muting and unmuting sounds,—can only be accomplished with Win32 API calls or functions in other unmanaged DLLs (Dynamic Link Libraries). Unmanaged code is code that runs outside of the Common Language Runtime (CLR). Examples include COM and ActiveX components written in C or C++ as well as Win32 API functions.

The samples in this article and the sample code provide the functionality for muting/unmuting sound, changing the windows resolution, displaying balloon tip windows, checking the power status of your computer, changing the wallpaper, and extracting icons from DLL or .exe files. Since this article was first written, displaying balloon tips has been added to the support of the Base Class Library, however the example is kept here for its instructional purposes. Using these samples as a starting point, you should be able to use the cool features available in other Win32 API calls or other unmanaged DLLs that are not exposed through the BCL. The code samples for each example provided in this article are only segments of the full code used in each sample and are used to illustrate a particular step. The full code is provided in a set of downloadable sample applications.

What is P/Invoke?

P/Invoke is short for Platform Invoke and provides the functionality to access functions, structs, and callbacks in unmanaged DLLs. P/Invoke provides a translation layer to assist developers by allowing them to extend the library of available functionality beyond the managed library of the BCL. To easiest way to understand how to use P/Invoke is to look at some sample applications that use P/Invoke to accomplish a task not possible with managed code.

An Overview of the Samples

Now that we have some background information about P/Invoke let us turn to the sample code that accompanies this article. Each sample is organized in a similar way and is designed to be added and used in your application without much modification. All of the external function declarations, structs, and class definitions used by those functions are in a class named NativeMethods. A single class provides a wrapper for all of the P/Invoke calls and is named the same as the sample (for example, Balloon in the Balloon sample). Each sample also includes all supporting classes and enumerations in separate class files and a windows form that shows how to use the specific functionality provided by the P/Invoke code in a real application (Except the Icon sample, which is used in the Power sample).

Task 1: Mute and Unmute Sound

The Mute/Unmute sound sample provides a programmatic way to mute and unmute sound on a computer. It uses a number of functions contained in the winmm.dll (mm means multimedia) to accomplish this. Since there are no mute or unmute functions we can create these ourselves using the xxxGetVolume and xxxSetVolume commands for the various sound streams. The three sound streams that are accessible through the winmm.dll are wave, midi and aux.

The Mute/Unmute sound sample is the simplest P/Invoke sample provided in this article, as it does not require any additional structures or classes to be defined.

Figure 1

Usage

After adding the sample code to your application, using the mute and unmute functionality is very simple. First, create a SoundDevice object passing in one of the three possible sound stream types: wave, aux, or midi. Then simply call the Mute and UnMute methods. You can also set the Volume property of the SoundDevice to adjust the volume for each of the sound streams.

[C#]

SoundDevice wave = new SoundDevice(DeviceTypes.Wave);

wave.Mute();

wave.UnMute();

wave.Volumne = 5;

Visual Basic

Dim wave As New SoundDevice(DeviceTypes.Wave)

wave.Mute()

wave.UnMute()

wave.Volumne = 5

Declarations

The first step is to identify the DLL and unmanaged functions we are going to use. For the wave sound stream type the two functions are waveOutGetVolume and waveOutSetVolume. Both functions reside in winmm.dll and their unmanaged signatures are as follows:

MMRESULT waveOutGetVolume(
  HWAVEOUT hwo,      
  LPDWORD pdwVolume  
);

MMRESULT waveOutSetVolume(
  HWAVEOUT hwo,  
  DWORD dwVolume 
);

We also use the similar functions for the aux and midi devices.

A second and slightly more complex hurdle to overcome in being able to access these functions from managed code is to figure out how to map the parameters and return values, such as LPDWORD and DWORD, to managed types. This is easily done by referring to the type conversion table provided in the Background Information section at the end of this article. From that table we find that the LPDWORD and DWORD types can be represented by the UInt32 managed type. The HWAVEOUT type is simply a HANDLE to an open waveform-audio output device. From the table we find that HANDLEs are represented by the IntPtr managed type. By consulting the documentation or header files for these APIs we can find that the MMRESULT type is an integer value that is set to one of the MMSYSERR_ values as the return value of the function. On the managed side, we will convert these "magic" integers into constants that make their meaning clearer. Most of the time you should either convert the return values into constants or, better yet, an enumeration to help identify their meaning in your managed code. In the mute/unmute sound example we have defined the possible return values in the MMSYSERR_ set of constants.

Now that we have the information we need, we declare the managed P/Invoke function wrappers as follows:

[C#]

[DllImport("winmm.dll")]
public static extern int waveOutGetVolume(IntPtr hwo, out uint dwVolume);

[DllImport("winmm.dll")]
public static extern int waveOutSetVolume(IntPtr hwo, uint dwVolume);


[Visual Basic]

<DllImport("winmm.dll")> _
Public Shared Function waveOutGetVolume(ByVal hwo As IntPtr, _
    ByRef dwVolume As System.UInt32) As Integer
End Function

<DllImport("winmm.dll")> _
Public Shared Function waveOutSetVolume(ByVal hwo As IntPtr, 
    ByVal dwVolume As System.UInt32) As Integer
End Function

As noted previously, these declarations reside in the NativeMethods class.

Implementation

These functions are called from the SoundDevice class GetVolume and SetVolume methods. This class provides an easy to use wrapper around the external functions in the NativeMethods class. Wrapping access to P/Invoke functions is a recommended best practice, as it provides for clean separation between managed and unmanaged functionality. You can also separate all error handling concerning the unmanaged code in the wrapper classes and throw CLR exceptions instead.

[C#]

private int GetVolume(ref ushort volLeft, ref ushort volRight)
{
    uint vol = 0;
    int result = 9999;

    switch (_type)
    {
    case DeviceTypes.Wave:
        result = NativeMethods.waveOutGetVolume(_hwo, out vol);
        break;
    case DeviceTypes.Aux:
        result = NativeMethods.auxGetVolume(_deviceID, out vol);
        break;
    case DeviceTypes.Midi:
        result = NativeMethods.midiOutGetVolume(_hwo, out vol);
        break;
    }

    if (result != NativeMethods.MMSYSERR_NOERROR)
        return result;

    volLeft = (ushort)(vol & 0x0000ffff);
    volRight = (ushort)(vol >> 16);
    return NativeMethods.MMSYSERR_NOERROR;
}

private int SetVolume(ushort volLeft, ushort volRight)
{
    uint vol = ((uint)volLeft & 0x0000ffff) | ((uint)volRight << 16);

    switch (_type)
    {
    case DeviceTypes.Wave:
        return NativeMethods.waveOutSetVolume(_hwo, vol);
    case DeviceTypes.Aux:
        return NativeMethods.auxSetVolume(_deviceID, vol);
    case DeviceTypes.Midi:
        return NativeMethods.midiOutSetVolume(_hwo, vol);
    }
    return 0;
}

[Visual Basic]

Private Function GetVolume(ByRef volLeft As System.UInt16, _
    ByRef volRight As System.UInt16) As Integer

    Dim vol As System.UInt32 = 0
    Dim result As Integer = 9999

    Select Case m_type
        Case DeviceTypes.Wave
            result = NativeMethods.waveOutGetVolume(m_hwo, vol)
        Case DeviceTypes.Aux
            result = NativeMethods.auxGetVolume(m_deviceID, vol)
        Case DeviceTypes.Midi
            result = NativeMethods.midiOutGetVolume(m_hwo, vol)
    End Select

    If Not (result = NativeMethods.MMSYSERR_NOERROR) Then
        Return result
    End If

    volLeft = CType((vol And &HFFFF), System.UInt16)
    volRight = CType((vol >> 16), System.UInt16)

    Return NativeMethods.MMSYSERR_NOERROR

End Function

Private Function SetVolume(ByVal volLeft As System.UInt16, _
    ByVal volRight As System.UInt16) As Integer

    Dim vol As System.UInt32 = (CType(volLeft, System.UInt32) And 65535) _
        Or (CType(volRight, System.UInt32) << 16)

    Select Case m_type
        Case DeviceTypes.Wave
            Return NativeMethods.waveOutSetVolume(m_hwo, vol)
        Case DeviceTypes.Aux
            Return NativeMethods.auxSetVolume(m_deviceID, vol)
        Case DeviceTypes.Midi
            Return NativeMethods.midiOutSetVolume(m_hwo, vol)
    End Select

    Return 0

End Function

Muting sound is really the same as changing the volume to zero. Before we can set the volume level to zero, however, we must first store the current volume level so we can reset the volume level when we unmute the sound. Unmuting simply sets the sound to the value stored during the mute step. These two functions are further abstracted in the Mute and UnMute methods.

[C#]

public void Mute()
{
    _leftVol = 0;
    _rightVol = 0;

    // First store the current volume settings
    int returnValue = GetVolume(ref _leftVol, ref _rightVol);

    // If that was successful then set the volume to zero
    if (returnValue == NativeMethods.MMSYSERR_NOERROR)
    {
        returnValue = SetVolume(0, 0);

        if (returnValue == NativeMethods.MMSYSERR_NOERROR)
        {
            // all is ok
        }
        else
        {
            MessageBox.Show("Could not set the volume to zero", 
                "Sound Sample", 
                MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
    else
    {
         MessageBox.Show("Could not get the current volume setting", 
            "Sound Sample", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

public void UnMute()
{
    if (_leftVol > 0 || _rightVol > 0)
    {
        int returnValue = SetVolume(_leftVol, _rightVol);

        if (returnValue == NativeMethods.MMSYSERR_NOERROR)
        {
            // all is ok
        }
        else
        {
            MessageBox.Show("Could not unmute the sound", 
                "Sound Sample",
                MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}

[Visual Basic]

Public Sub Mute()
    m_leftVol = 0
    m_rightVol = 0

    Dim returnValue As Integer = GetVolume(m_leftVol, m_rightVol)

    If returnValue = NativeMethods.MMSYSERR_NOERROR Then
        returnValue = SetVolume(0, 0)
        If returnValue = NativeMethods.MMSYSERR_NOERROR Then
            ' all is ok
        Else
            MessageBox.Show("Could not set the volume to zero", _
                "Sound Sample", _
                MessageBoxButtons.OK, MessageBoxIcon.Error)
        End If
    Else
        MessageBox.Show("Could not get the current volume setting", _
            "Sound Sample", MessageBoxButtons.OK, MessageBoxIcon.Error)
    End If
End Sub

Public Sub UnMute()
    If m_leftVol > 0 OrElse m_rightVol > 0 Then

        Dim returnValue As Integer = SetVolume(m_leftVol, m_rightVol)

        If returnValue = NativeMethods.MMSYSERR_NOERROR Then
            ' all is ok
        Else
            MessageBox.Show("Could not unmute the sound", _
                "Sound Sample", _
                MessageBoxButtons.OK, MessageBoxIcon.Error)
        End If
    End If
End Sub

In addition to muting and unmuting the sound, this sample also allows you to adjust the volume using the slider control, which also calls the xxxGetVolume and xxxSetVolume methods.

The mute/unmute sound example is probably the easiest possible use of P/Invoke. Other than declaring the functions as extern (in C#) or shared (in Visual Basic) and referencing the System.Runtime.InteropServices namespace, no further action is required to start using the new functionality. Most of the work is translating the unmanaged method signatures to the managed signatures. A more common scenario however, involves the use of structures or classes that are passed back and forth and contain the data required to execute the functions.

Luckily, sites such as PInvoke.net provide a large set of predefined functions in both C# and Visual Basic .NET to make this easier. However, for the sake of explanation, this article will create all of these wrappers from scratch.

Task 2: Changing the Display Resolution

The Windows Resolution example uses the EnumDisplaySettings and ChangeDisplaySettings functions from the user32.dll library to programmatically enumerate and change the display settings. This is the same functionality usually accessed by right-clicking the desktop, selecting Properties | Settings, and adjusting the Screen Resolution slider.

Figure 2

Usage

After instantiating the Display object you can retrieve all available display types and store them in a DevMode List by calling the GetDisplaySettings method. To change the resolution, simply pass a DevMode structure containing the new settings to the ChangeSettings method. You can use one of the DevMode structures returned in the GetDisplaySettings call or create a new structure.

[C#]

Display display = new Display();


List<DevMode> settings = display.GetDisplaySettings();

display.ChangeSettings(selectedMode);

[Visual Basic]

Dim display As New Display()

Dim settings As List(Of DevMode) = display.GetDisplaySettings()

display.ChangeSettings(selectedMode)

Declarations

Both the Win32 EnumDisplaySettings and ChangeDisplaySettings functions use a DEVMODE structure that contains all display device or printer device specific settings. This structure is used to return the setting information in the EnumDisplaySettings function and to change the settings in the ChangeDisplaySetting function. The unmanaged definition of the DEVMODE structure is as follows.

typedef struct _devicemode { 
  BCHAR  dmDeviceName[CCHDEVICENAME]; 
  WORD   dmSpecVersion; 
  WORD   dmDriverVersion; 
  WORD   dmSize; 
  WORD   dmDriverExtra; 
  DWORD  dmFields; 
  union {
    struct {
      short dmOrientation;
      short dmPaperSize;
      short dmPaperLength;
      short dmPaperWidth;
      short dmScale; 
      short dmCopies; 
      short dmDefaultSource; 
      short dmPrintQuality; 
    };
    POINTL dmPosition;
    DWORD  dmDisplayOrientation;
    DWORD  dmDisplayFixedOutput;
  };

  short  dmColor; 
  short  dmDuplex; 
  short  dmYResolution; 
  short  dmTTOption; 
  short  dmCollate; 
  BYTE  dmFormName[CCHFORMNAME]; 
  WORD  dmLogPixels; 
  DWORD  dmBitsPerPel; 
  DWORD  dmPelsWidth; 
  DWORD  dmPelsHeight; 
  union {
    DWORD  dmDisplayFlags; 
    DWORD  dmNup;
  }
  DWORD  dmDisplayFrequency; 
#if(WINVER >= 0x0400) 
  DWORD  dmICMMethod;
  DWORD  dmICMIntent;
  DWORD  dmMediaType;
  DWORD  dmDitherType;
  DWORD  dmReserved1;
  DWORD  dmReserved2;
#if (WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400)
  DWORD  dmPanningWidth;
  DWORD  dmPanningHeight;
#endif
#endif /* WINVER >= 0x0400 */
} DEVMODE;

The unions and the preprocess condition sections of the DEVMODE struct is something we don't need when dealing with display settings, so we assume we're going to be run with ((WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400)) being true, and the portions of the union that apply to display devices . The resulting managed struct definition contains only the information we need.

[C#]

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DevMode
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string dmDeviceName;

    public short dmSpecVersion;
    public short dmDriverVersion;
    public short dmSize;
    public short dmDriverExtra;
    public int dmFields;
    public int dmPositionX;
    public int dmPositionY;
    public int dmDisplayOrientation;
    public int dmDisplayFixedOutput;
    public short dmColor;
    public short dmDuplex;
    public short dmYResolution;
    public short dmTTOption;
    public short dmCollate;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string dmFormName;

    public short dmLogPixels;
    public short dmBitsPerPel;
    public int dmPelsWidth;
    public int dmPelsHeight;
    public int dmDisplayFlags;
    public int dmDisplayFrequency;
    public int dmICMMethod;
    public int dmICMIntent;
    public int dmMediaType;
    public int dmDitherType;
    public int dmReserved1;
    public int dmReserved2;
    public int dmPanningWidth;
    public int dmPanningHeight;
}

[Visual Basic]

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)> _
    Public Structure DevMode

        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)> _
        Public dmDeviceName As String

        Public dmSpecVersion As Short
        Public dmDriverVersion As Short
        Public dmSize As Short
        Public dmDriverExtra As Short
        Public dmFields As Integer
        Public dmPositionX As Integer
        Public dmPositionY As Integer
        Public dmDisplayOrientation As Integer
        Public dmDisplayFixedOutput As Integer
        Public dmColor As Short
        Public dmDuplex As Short
        Public dmYResolution As Short
        Public dmTTOption As Short
        Public dmCollate As Short

        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)> _
        Public dmFormName As String

        Public dmLogPixels As Short
        Public dmBitsPerPel As Short
        Public dmPelsWidth As Integer
        Public dmPelsHeight As Integer
        Public dmDisplayFlags As Integer
        Public dmDisplayFrequency As Integer
        Public dmICMMethod As Integer
        Public dmICMIntent As Integer
        Public dmMediaType As Integer
        Public dmDitherType As Integer
        Public dmReserved1 As Integer
        Public dmReserved2 As Integer
        Public dmPanningWidth As Integer
        Public dmPanningHeight As Integer
    End Structure

The structure is decorated with the StructLayout attribute and the sequential setting for the Layout property. For further details concerning the StructLayout attribute, please see the discussion in the Background Information section at the end of this article. Most of the unmanaged member types were easily converted to managed using the type conversion table. Only the dmDeviceName and the dmFormName fields are a little more difficult because they are fixed length arrays and have no direct match to a managed type. When we encounter this situation we can use the UnmanagedType property of the MarshalAs attribute to define the type as one of the types in the UnmanagedType enumeration. The ByValTStr type is defined as "A fixed-length array of characters; the array's type is determined by the character set of the containing structure." Using this type and the SizeConst property to set the size of the array we can adequately define the type for the marshaller. Note that the CharSet property defined in the StructLayout also determines the character set of this type. The default setting is Char.Ansi, but this setting causes some performance issues under WIN NT and you should use the CharSet.Auto setting whenever possible to let the marshaller determine the best character set to use.

Implementation

Once we have defined the functions and structures the application flow is pretty straightforward. First, we call the GetSettings function, which will in turn call our P/Invoke declaration for EnumDisplaySettings. A counter is passed in as the iModeNum parameter. We increment this counter until the function returns a 0, which indicates that all of the possible display settings available for the computer have been returned and stored in the generic list of type DevMode.

[C#]

List<DevMode> modes = new List<DevMode>();
DevMode devmode = this.DevMode;
int counter = 0;
int returnValue = 1;

while (returnValue != 0)
{
    returnValue = GetSettings(ref devmode, counter++);
    modes.Add(devmode);
}

[Visual Basic]

Dim modes As New List(Of DevMode)
Dim devmode As DevMode = Me.DevMode
Dim counter As Integer = 0
Dim returnValue As Integer = 1

While returnValue <> 0
    returnValue = GetSettings(devmode, counter)
    modes.Add(devmode)
    counter = counter + 1
End While    

To change the current setting all we have to do is pass a DevMode struct containing the new settings to the ChangeDisplaySettings function. We stored the various DevMode structures in a list so they can be easily retrieved. We compare the return value to the ReturnCodes enumeration to determine the outcome of the call. Once again, it is best to convert the various "magic" numbers into enumerations or constants in the managed code to make them easier to understand; otherwise you might later on wonder what a return code of -5 actually means.

[C#]

public string ChangeSettings(DevMode devmode)
{
    string errorMessage = "";

    ReturnCodes iRet = NativeMethods.ChangeDisplaySettings(ref devmode, 0);

    switch (iRet)
    {
    case ReturnCodes.DISP_CHANGE_SUCCESSFUL:
        break;
    case ReturnCodes.DISP_CHANGE_RESTART:
        errorMessage = "Please restart your system";
        break;
    case ReturnCodes.DISP_CHANGE_FAILED:
        errorMessage = "ChangeDisplaySettigns API failed";
        break;
    case ReturnCodes.DISP_CHANGE_BADDUALVIEW:
        errorMessage = "The settings change was unsuccessful because " +
            "system is DualView capable.";
        break;
    case ReturnCodes.DISP_CHANGE_BADFLAGS:
        errorMessage = "An invalid set of flags was passed in.";
        break;
    case ReturnCodes.DISP_CHANGE_BADPARAM:
        errorMessage = "An invalid parameter was passed in. " +
            "This can include an invalid flag or combination of flags.";
        break;
    case ReturnCodes.DISP_CHANGE_NOTUPDATED:
        errorMessage = "Unable to write settings to the registry.";
        break;
    default:
        errorMessage 
            = "Unknown return value from ChangeDisplaySettings API";
        break;
    }
    return errorMessage;
}

[Visual Basic]

Public Function ChangeSettings(ByVal devmode As DevMode) As String
    Dim errorMessage As String = ""

    Dim iRet As ReturnCodes = NativeMethods.ChangeDisplaySettings(devmode, 0)

    Select Case iRet
    Case ReturnCodes.DISP_CHANGE_SUCCESSFUL
        Exit Function
    Case ReturnCodes.DISP_CHANGE_RESTART
        errorMessage = "Please restart your system"
        Exit Function
    Case ReturnCodes.DISP_CHANGE_FAILED
        errorMessage = "ChangeDisplaySettigns API failed"
        Exit Function
    Case ReturnCodes.DISP_CHANGE_BADDUALVIEW
        errorMessage = "The settings change was unsuccessful because " + _
            "system is DualView capable."
        Exit Function
    Case ReturnCodes.DISP_CHANGE_BADFLAGS
        errorMessage = "An invalid set of flags was passed in."
        Exit Function
    Case ReturnCodes.DISP_CHANGE_BADPARAM
        errorMessage = "An invalid parameter was passed in. " + _
            "This can include an invalid flag or combination of flags."
        Exit Function
    Case ReturnCodes.DISP_CHANGE_NOTUPDATED
        errorMessage = "Unable to write settings to the registry."
        Exit Function
    Case Else
        errorMessage = "Unknown return value from ChangeDisplaySettings API"
        Exit Function
    End Select
    Return errorMessage
End Function

That is all there is to this P/Invoke sample. Using structs to pass information back and forth rather than lots of single parameters is the more common scenario in unmanaged code.

For our last example let us look at a more advanced use of P/Invoke.

Task 3: Showing Balloon tips

The Balloon tips sample provides a way to display the Balloon type pop-up windows used in Microsoft Windows. The tips are commonly used for data entry validation and appear over entry fields with missing or incorrect values. The sample allows you to change the alignment, icon, positions, and stem location to produce different balloon tips windows.

Figure 3

Usage

You can create a new balloon tip by simply creating a new Balloon object, setting the appropriate values for the ParentControl, Title, Text, Icon, Position, Alignment, IsPostionAbsolute, and IsStemCentered properties and then calling the Show method.

To remove the Balloon tip, call the Dispose method.

[C#]

Balloon balloon = new Balloon();
balloon.ParentControl = this.parentTextBox;
balloon.Title = "This is the balloon title";
balloon.Text = "This is where the balloon text appears";
balloon.Icon = BalloonIcon.Info;
balloon.Alignment = BalloonAlignment. TopMiddle;
balloon.IsPositionAbsolute = true;
balloon.IsStemCentered = false;

// show the balloon tip
balloon.Show();

// Hide the balloon tip
ballon.Dispose();

[Visual Basic]

Dim balloon as New Balloon();
balloon.ParentControl = this.parentTextBox;
balloon.Title = "This is the balloon title";
balloon.Text = "This is where the balloon text appears";
balloon.Icon = BalloonIcon.Info;
balloon.Alignment = BalloonAlignment. TopMiddle;
balloon.IsPositionAbsolute = true;
balloon.IsStemCentered = false;

' show the balloon tip
balloon.Show();

' Hide the balloon tip
ballon.Dispose();

Declarations

This sample uses four Win32 API functions (SendMessage, GetClientRect, ClientToScreen, and SetWindowPos) and two structures (TOOLINFO and RECT) to display the balloon tip window.

[C#]

 [DllImport("User32", SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam,
    IntPtr lParam);

[DllImport("User32", SetLastError = true)]
public static extern int GetClientRect(IntPtr hWnd, ref RECT lpRect);

[DllImport("User32", SetLastError = true)]
public static extern int ClientToScreen(IntPtr hWnd, ref RECT lpRect);

[DllImport("User32", SetLastError = true)]
public static extern int SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, 
    int X, int Y, int cx, int cy, int uFlags);

[StructLayout(LayoutKind.Sequential)]
public struct TOOLINFO
{
    public int cbSize;
    public int uFlags;
    public IntPtr hwnd;
    public IntPtr uId;
    public RECT rect;
    public IntPtr hinst;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpszText;
    public uint lParam;
}

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int left;
    public int top;
    public int right;
    public int bottom;
}

[Visual Basic]

<DllImport("User32", SetLastError:=True)> _
Public Shared Function SendMessage(ByVal hWnd As IntPtr, _ 
    ByVal Msg As Integer, ByVal wParam As Integer, _
    ByVal lParam As IntPtr) As Integer
End Function

<DllImport("User32", SetLastError:=True)> _
Public Shared Function GetClientRect(ByVal hWnd As IntPtr, _ 
    ByRef lpRect As RECT) As Integer
End Function

<DllImport("User32", SetLastError:=True)> _
Public Shared Function ClientToScreen(ByVal hWnd As IntPtr, ByRef lpRect _
    As RECT) As Integer
End Function

<DllImport("User32", SetLastError:=True)> _
Public Shared Function SetWindowPos(ByVal hWnd As IntPtr, _
    ByVal hWndInsertAfter As IntPtr, ByVal X As Integer, _
    ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, _
    ByVal uFlags As Integer) As Integer
End Function
<StructLayout(LayoutKind.Sequential)> _
Public Structure TOOLINFO
Public cbSize As Integer
Public uFlags As Integer
Public hwnd As IntPtr
Public uId As IntPtr
Public rect As RECT
Public hinst As IntPtr
<MarshalAs(UnmanagedType.LPTStr)> _
Public lpszText As String
Public lParam As System.UInt32
End Structure

<StructLayout(LayoutKind.Sequential)> _
Public Structure RECT
Public left As Integer
Public top As Integer
Public right As Integer
Public bottom As Integer
End Structure

Implementation

In addition to wrapping these Win32 API functions and structures, we also need to create the BalloonWindow, which will be a new class that inherits from the .NET Framework's NativeWindow class. This class provides a low-level encapsulation of a window handle and a window procedure. We use this class to determine if the user clicked on the balloon tip. If this occurs, a custom event (WindowClosed) is raised and ultimately causes the balloon tip to be closed. The WndProc method is called every time a windows message is sent to the handle of the window.

[C#]

internal class BalloonWindow : NativeWindow
{
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_LBUTTONDOWN)
        {
            if (WindowClosed != null)
                WindowClosed();
        }
        base.WndProc(ref m);
    }

    private const int WM_LBUTTONDOWN = 0x0201;
    public event BalloonWindowClosedEventHandler WindowClosed;
}

[Visual Basic]

Friend Class BalloonWindow
   Inherits NativeWindow
    Protected Overloads Overrides Sub WndProc(ByRef m As Message)
        If m.Msg = WM_LBUTTONDOWN Then
            RaiseEvent WindowClosed()
        End If
        MyBase.WndProc(m)
    End Sub

    Private Const WM_LBUTTONDOWN As Integer = &H201
    Public Event WindowClosed As BalloonWindowClosedEventHandler
End Class

In addition to the usual constants, this sample also uses three enumerations (BalloonAlignment, BalloonIcon, and BalloonPosition) to help us in determining the position and Icon used for the window. With all the supporting elements defined we can now create the balloon tip window. The Balloon class encapsulates all of the required functionality. It provides the two methods we need to show and close a window and a number of properties used to describe each window.

The Show method first creates a window using the BalloonWindow class. The NativeWindow CreateHandle method is basically a wrapper around the Win32 CreateWindowEx function. It requires us to pass in a CreateParams object that defines the various style elements of the new window.

[C#]

CreateParams createParams = new CreateParams();
createParams.ClassName = NativeMethods.TOOLTIPS_CLASS;
createParams.Style = NativeMethods.WS_POPUP | NativeMethods.TTS_BALLOON | 
    NativeMethods.TTS_NOPREFIX | NativeMethods.TTS_ALWAYSTIP | 
    NativeMethods.TTS_CLOSE;

_balloonWindow.CreateHandle(createParams);

[Visual Basic]

Dim createParams As CreateParams = New CreateParams
createParams.ClassName = NativeMethods.TOOLTIPS_CLASS
createParams.Style = NativeMethods.WS_POPUP Or NativeMethods.TTS_BALLOON _
    Or NativeMethods.TTS_NOPREFIX Or NativeMethods.TTS_ALWAYSTIP Or _
    NativeMethods.TTS_CLOSE

m_balloonWindow.CreateHandle(createParams)

The next step is to create and populate a TOOLINFO structure. Notice how we use the constant to make the flags easier to understand.

[C#]

_toolInfo = new TOOLINFO();
_toolInfo.cbSize = Marshal.SizeOf(_toolInfo);
toolInfo.uFlags = NativeMethods.TTF_TRACK | NativeMethods.TTF_IDISHWND | 
    NativeMethods.TTF_TRANSPARENT | NativeMethods.TTF_SUBCLASS | 
    NativeMethods.TTF_PARSELINKS;

if (this.IsPositionAbsolute)
{
    toolInfo.uFlags |= NativeMethods.TTF_ABSOLUTE;
}
if (this._isStemCentered)
{
    toolInfo.uFlags |= NativeMethods.TTF_CENTERTIP;
}
_toolInfo.uId = _balloonWindow.Handle;
_toolInfo.lpszText = this.Text;
_toolInfo.hwnd = _parentControl.Handle;

[Visual Basic]

m_toolInfo = New TOOLINFO
m_toolInfo.cbSize = Marshal.SizeOf(m_toolInfo)
m_toolInfo.uFlags = NativeMethods.TTF_TRACK Or NativeMethods.TTF_IDISHWND _
    Or NativeMethods.TTF_TRANSPARENT Or NativeMethods.TTF_SUBCLASS Or _
    NativeMethods.TTF_PARSELINKS

If Me.IsPositionAbsolute Then
    m_toolInfo.uFlags = m_toolInfo.uFlags Or (NativeMethods.TTF_ABSOLUTE)
End If
If Me.m_isStemCentered Then
    m_toolInfo.uFlags = m_toolInfo.uFlags Or (NativeMethods.TTF_CENTERTIP)
End If
m_toolInfo.uId = m_balloonWindow.Handle
m_toolInfo.lpszText = Me.Text
m_toolInfo.hwnd = m_parentControl.Handle

With the BalloonWindow and TOOLINFO structure defined, we then call three Win32 APIs to set the size and position of the window.

[C#]

NativeMethods.GetClientRect(_parentControl.Handle, ref _toolInfo.rect);
NativeMethods.ClientToScreen(_parentControl.Handle, ref _toolInfo.rect);
NativeMethods.SetWindowPos(_balloonWindow.Handle, 
    NativeMethods.HWND_TOPMOST, 0, 0, 0, 0, NativeMethods.SWP_NOACTIVATE | 
    NativeMethods.SWP_NOMOVE | NativeMethods.SWP_NOSIZE);

[Visual Basic]

NativeMethods.GetClientRect(m_parentControl.Handle, _
    m_toolInfo.rect)NativeMethods.ClientToScreen(m_parentControl.Handle, _
    m_toolInfo.rect)
NativeMethods.SetWindowPos(m_balloonWindow.Handle, _
    NativeMethods.HWND_TOPMOST, 0, 0, 0, 0, NativeMethods.SWP_NOACTIVATE _
    Or NativeMethods.SWP_NOMOVE Or NativeMethods.SWP_NOSIZE)

Next, we send the required information to the window to turn it from an ordinary window into a balloon tip window. The Win32 SendMessage API is often used to communicate with an existing window and is used here to accomplish our task. First, we pass in the TOOLINFO structure, then we set the maximum width for the window and finally we set the title. The last step is to explicitly clear the memory allocated in the AllocHGlobal and StringToHGlobalAuto methods by calling the FreeHGlobal method.

[C#]

IntPtr pointer = Marshal.AllocHGlobal(Marshal.SizeOf(_toolInfo));
Marshal.StructureToPtr(_toolInfo, pointer, true);
NativeMethods.SendMessage(_balloonWindow.Handle, 
    NativeMethods.TTM_ADDTOOL, 0, pointer);
toolInfo = (TOOLINFO)Marshal.PtrToStructure(pointer, typeof(TOOLINFO));

NativeMethods.SendMessage(_balloonWindow.Handle, 
    NativeMethods.TTM_SETMAXTIPWIDTH, 0, new IntPtr(_maxWidth));
IntPtr pointerTitle = Marshal.StringToHGlobalAuto(this.Title);

NativeMethods.SendMessage(_balloonWindow.Handle, 
    NativeMethods.TTM_SETTITLE, (int)this.Icon, pointerTitle);
this.SetBalloonPosition(_toolInfo.rect);

Marshal.FreeHGlobal(pointer);
Marshal.FreeHGlobal(pointerTitle);

[Visual Basic]

Dim pointer As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(m_toolInfo))
Marshal.StructureToPtr(m_toolInfo, pointer, True)
NativeMethods.SendMessage(m_balloonWindow.Handle, _
    NativeMethods.TTM_ADDTOOL, 0, pointer)
m_toolInfo = CType(Marshal.PtrToStructure(pointer, GetType(TOOLINFO)), _
    TOOLINFO)
NativeMethods.SendMessage(m_balloonWindow.Handle, _
    NativeMethods.TTM_SETMAXTIPWIDTH, 0, New IntPtr(m_maxWidth))

Dim pointerTitle As IntPtr = Marshal.StringToHGlobalAuto(Me.Title)
NativeMethods.SendMessage(m_balloonWindow.Handle, _
    NativeMethods.TTM_SETTITLE, CType(Me.Icon, Integer), pointerTitle)
Me.SetBalloonPosition(m_toolInfo.rect)
Marshal.FreeHGlobal(pointer)
Marshal.FreeHGlobal(pointerTitle)

The last step is to actually display the window. This is accomplished by passing the TTM_TRACKACTIVATE message to the tool window (the edit control) that the balloon tip is associated with. To do so, we once again use the SendMessage function. Remember to free the allocated memory using the FreeHGlobal method to avoid memory leaks.

[C#]

IntPtr pointer2 = Marshal.AllocHGlobal(Marshal.SizeOf(_toolInfo));
Marshal.StructureToPtr(_toolInfo, pointer2, true);
NativeMethods.SendMessage(_balloonWindow.Handle, _
    NativeMethods.TTM_TRACKACTIVATE, -1, pointer2);

Marshal.FreeHGlobal(pointer2);

[Visual Basic]

Dim pointer2 As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(m_toolInfo))
Marshal.StructureToPtr(m_toolInfo, pointer2, True)
NativeMethods.SendMessage(m_balloonWindow.Handle, _
    NativeMethods.TTM_TRACKACTIVATE, -1, pointer2)
Marshal.FreeHGlobal(pointer2)

That's it! We have a Balloon tip window.

Luckily, closing the window is much easier than opening it. We simply call the SendMessage function passing in the same TTM_TRACKACTIVATE constant, but instead of the -1 we pass a 0 for the wParam parameter.

[C#]

NativeMethods.SendMessage(_balloonWindow.Handle, 
    NativeMethods.TTM_TRACKACTIVATE, 0, pointer);

[Visual Basic]

NativeMethods.SendMessage(m_balloonWindow.Handle, _
    NativeMethods.TTM_TRACKACTIVATE, 0, pointer)

Background Information

DLLImport Attribute

The basic steps of using P/Invoke always involve creating a managed function that is decorated with the DllImport attribute. In C# these unmanaged wrapper functions must be declared as extern. In Visual Basic they are declared as Shared. When this function is called, the CLR locates and loads the required DLLs, marshals the parameters, makes the call, and finally unmarshals any out-parameters and return values and finally returns to your managed application.

[C#]

[DllImport("Shell32.dll", EntryPoint = "ExtractIconExW", 
    CharSet = CharSet.Unicode, ExactSpelling = true, 
    CallingConvention = CallingConvention.StdCall)]
public static extern int ExtractIconEx(string sFile, int iIndex, 
    out IntPtr piLargeVersion, out IntPtr piSmallVersion, int amountIcons);

[Visual Basic]

<DllImport("Shell32.dll", EntryPoint:="ExtractIconExW", _
    CharSet:=CharSet.Unicode, ExactSpelling:=True, _
    CallingConvention:=CallingConvention.StdCall)> _
Public Shared Function ExtractIconEx(ByVal sFile As String, _
    ByVal iIndex As Integer, ByRef piLargeVersion As IntPtr, _
    ByRef piSmallVersion As IntPtr, ByVal amountIcons As Integer) _
    As Integer
End Function

The DllImport attribute is used to decorate a managed method to indicate that it uses an unmanaged dynamic-link library (DLL) as a static entry point. As a minimum you must supply the name of the DLL providing the entry point. Let us look at each parameter in more detail.

  1. Name: The name of the DLL in which the function resides.
  2. Entry Point: The name or ordinal of the function. This is useful when aliasing the external declaration to avoid name conflicts.
  3. SetLastError: When set to true this causes the COM function to call the SetLastError function before returning. This value can then be read by the managed code.
  4. CharSet: This attribute determines how strings are marshaled. The default of CharSet.Auto converts string to ANSI on Win 9X operating systems and Unicode on Windows NT, 2000, and XP.
  5. Exact Spelling: Setting this to false allows the CLR to search for entry points in the DLL that are similar to the EntryPoint value.
  6. BestFitMapping: Turns on or off some optimization behavior when converting from Unicode to ANSI.
  7. Calling Convention: One of five possible values. The Winapi setting is the same as the default platform calling convention. This means that using Winapi and StdCall on a Windows platform are identical.
  8. Preserve Signature: When true, the signature of the external function is identical to that of the unmanaged function.
  9. ThrowOnUnmappableChar: If set to true, an exception is thrown when a Unicode character can not be represented in ANSI; otherwise the character is converted to a ? and no exception is thrown.

[C#]

[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW",  SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);

[Visual Basic]

<DllImport("KERNEL32.DLL", EntryPoint := "MoveFileW", _
   SetLastError := True, CharSet := CharSet.Unicode, _
   ExactSpelling := True, _
   CallingConvention := CallingConvention.StdCall)> _
Public Shared Function MoveFile(src As String, dst As String) As Boolean
    ' Leave function empty - DLLImport attribute 
    ' forwards calls to MoveFile to MoveFileW in KERNEL32.DLL.
End Function

Marshaling

Marshalling is the act of converting data such that it can be passed and correctly analyzed between managed and unmanaged program spaces. This marshaling is performed at runtime using the CLR's marshalling service. When using P/Invoke you typically need to marshal classes and structs and control marshalling details using a number of attributes from the System.Runtime.InteropServices namespace.

Type Conversions

During marshaling one of the most important steps is converting unmanaged types to managed types and vice versa. The CLR marshaling service knows how to perform many of these conversions for you, but you must still know how the various types match up to each other when converting the unmanaged signature to the managed function. You can use this conversion table to match up the various types.

Table 1

Windows Data Type .NET Data Type
BOOL, BOOLEAN Boolean or Int32
BSTR String
BYTE Byte
CHAR Char
DOUBLE Double
DWORD/LPDWORD Int32 or UInt32
FLOAT Single
HANDLE (and all other handle types, such as HFONT and HMENU) IntPtr, UintPtr, or HandleRef
HRESULT Int32 or UInt32
INT Int32
LANGID Int16 or UInt16
LCID Int32 or UInt32
LONG Int32
LPARAM IntPtr, UintPtr, or Object
LPCSTR String
LPCTSTR String
LPCWSTR String
LPSTR String or StringBuilder*
LPTSTR String or StringBuilder
LPWSTR String or StringBuilder
LPVOID IntPtr, UintPtr, or Object
LRESULT IntPtr
SAFEARRAY .NET array type
SHORT Int16
TCHAR Char
UCHAR SByte
UINT Int32 or UInt32
ULONG Int32 or UInt32
VARIANT Object
VARIANT_BOOL Boolean
WCHAR Char
WORD Int16 or UInt16
WPARAM IntPtr, UintPtr, or Object

Callbacks

In addition to calling unmanaged DLLs from managed code, you can use P/Invoke to have unmanaged code call back to managed code. This provides an asynchronous way to use unmanaged resources.

[C#]

delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, 
    IntPtr lParam);

[Visual Basic]

Delegate Function WndProc(ByVal hWnd As IntPtr, ByVal msg As Integer, _
     ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr

StructLayout Attribute

In addition to the simple types listed in the type conversion table, many unmanaged functions use structs to pass information more efficiently. The StructLayout attribute allows you to specify how to lay out the data members in managed portions of memory so that they can successfully be referenced in unmanaged portions of memory. The parameters are:

  1. Layout: Can be one of the following kinds, with sequential being the most common.
    1. LayoutKind.Sequential
    2. LayoutKind.Union
    3. LayoutKind.Explicit
  2. Pack: This value controls how the data fields are aligned in memory by defining the packing size of each field. The possible values are 1, 2, 4, 8, and 16. The default is 8 except for unmanaged structs that typically have a default packing size of 4.
  3. Charset: This attribute determines how strings are marshaled. The default of CharSet.Auto converts string to ANSI on Win 9X operating systems and Unicode on Windows NT, 2000, and XP. (Same as on the DLLImport attribute).
  4. Size: This is the size of the struct or class and must be at least as large as the sum of the size of the members.

[C#]

[StructLayout(LayoutKind.Explicit, Size=16, CharSet=CharSet.Ansi)]
public class MySystemTime 
{
   [FieldOffset(0)]public ushort wYear; 
   [FieldOffset(2)]public ushort wMonth;
   [FieldOffset(4)]public ushort wDayOfWeek; 
   [FieldOffset(6)]public ushort wDay; 
   [FieldOffset(8)]public ushort wHour; 
   [FieldOffset(10)]public ushort wMinute; 
   [FieldOffset(12)]public ushort wSecond; 
   [FieldOffset(14)]public ushort wMilliseconds; 
}

[Visual Basic]

<StructLayout(LayoutKind.Explicit, Size := 16, CharSet := CharSet.Ansi)>  _
Public Class MySystemTime
   <FieldOffset(0)> Public wYear As Short
   <FieldOffset(2)> Public wMonth As Short
   <FieldOffset(4)> Public wDayOfWeek As Short
   <FieldOffset(6)> Public wDay As Short
   <FieldOffset(8)> Public wHour As Short
   <FieldOffset(10)> Public wMinute As Short
   <FieldOffset(12)> Public wSecond As Short
   <FieldOffset(14)> Public wMilliseconds As Short
End Class 'MySystemTime

FieldOffset Attribute

When a struct or class uses the explicit layout setting, each field can be decorated with the FieldOffset attribute. It specifies the field offset in bytes from the start of the struct or class. It is allowable to make fields overlap, thus creating a union data structure that provides new value by combining the overlapping values from separate fields.

[C#]

[StructLayout(LayoutKind.Explicit)]
public class SYSTEM_INFO
{
    [FieldOffset(0)] public ulong OemId;
    [FieldOffset(4)] public ulong PageSize;
    [FieldOffset(16)] public ulong ActiveProcessorMask;
    [FieldOffset(20)] public ulong NumberOfProcessors;
    [FieldOffset(24)] public ulong ProcessorType;
}

[Visual Basic]

<StructLayout(LayoutKind.Explicit)> _
Public Class SYSTEM_INFO
    <FieldOffset(0)> Private OemId As System.UInt64
    <FieldOffset(4)> Private PageSize As System.UInt64
    <FieldOffset(16)> Private ActiveProcessorMask As System.UInt64
    <FieldOffset(20)> Private NumberOfProcessors As System.UInt64
    <FieldOffset(24)> Private ProcessorType As System.UInt64
End Class

MarshallAs Attribute

At times the exact type required by the unmanaged function does not exist in managed code nor can it be substituted or mapped to another managed type. At this point you have to manually provide the marshaller with the type information using the MarshallAs attribute. The MarshallAs attribute overrides the default marshaling behavior of the CLR marshalling service as we have done in the TOOLINFO struct in the balloon tip example.

[C#]

//Applied to a parameter.
  public void M1 ([MarshalAs(UnmanagedType.LPWStr)]String msg);
//Applied to a field within a class.
  class MsgText {
    [MarshalAs(UnmanagedType.LPWStr)] Public String msg;
  }
//Applied to a return value.
[return: MarshalAs(UnmanagedType.LPWStr)]
public String GetMessage();

[Visual Basic]

'Applied to a parameter.
  Public Sub M1 (<MarshalAs(UnmanagedType.LPWStr)> msg As String)
'Applied to a field within a class.
  Class MsgText 
    <MarshalAs(UnmanagedType.LPWStr)> Public msg As String
  End Class
'Applied to a a return value.
  Public Function M2() As <MarshalAs(UnmanagedType.LPWStr)> String

Conclusion

Using P/Invoke is a great way to extend the set of functionality available to you as a developer. With P/Invoke, you can leverage the tried and true functionality of the Win32 API and other unmanaged code and not have to reinvent the wheel in managed code. This can save you time and can provide functionality that is robust and well tested, adding to the overall usefulness of your application.

Of course, the downside is that much of the details concerning marshalling and freeing memory are suddenly thrust into the managed world when accessing unmanaged functionality. This can reduce the overall stability of your application and lengthen the development cycle, so be careful.

For more on P/Invoke, please refer to Calling Win32 DLLs in C# with P/Invoke in the MSDN online magazine.

Show:
© 2014 Microsoft