Paru le 01 mars 2006
Par Microsoft Corporation
Résumé : Apprenez à utiliser P/Invoke pour appeler des API Win32 non gérées à partir de code géré. Ces exemples d'applications illustrent comment couper le son, changer la résolution de Windows et afficher des info-bulles à partir de votre code géré. (33 pages imprimées)
Cliquez pour télécharger pinvokesamples.msi.
Cliquez pour lire l'article en anglais
.
Introduction
La bibliothèque de classes de base (BCL, Base Class Library) du .NET Framework (BCL) est constituée de milliers de classes qui couvrent presque toutes les fonctionnalités requises pour créer des applications puissantes. Certaines fonctionnalités, telles que le changement de résolution de Windows ou l'activation/la désactivation du son, ne peuvent être effectuées qu'avec des appels d'API ou de fonctions Win32 dans d'autres DLL (Dynamic Link Libraries) gérées. Le code non géré est celui qui réside en dehors du CLR (Common Language Runtime). Les composants COM et ActiveX écrits en C ou C++, ainsi que les fonctions des API Win32, en sont des exemples.
Les exemples de cet article et l'exemple de code permettent de désactiver/réactiver le son, de changer la résolution Windows, d'afficher des info-bulles, de déterminer l'état d'alimentation de votre ordinateur, de changer le papier peint et d'extraire les icônes des fichiers DLL ou .exe. Dans la mesure où cet article a d'abord été écrit, l'affichage des info-bulles a été ajouté à la prise en charge de la BCL, mais l'exemple est conservé ici à des informations d'information. En utilisant ces exemples comme point de départ, vous devriez pouvoir utiliser les fonctionnalités intéressantes de l'API Win32 ou d'autres DLL non gérées qui ne sont pas exposées via la BCL. Les échantillons de code de chaque exemple de cet article ne sont que des extraits du code complet utilisé dans chaque échantillon et sont utilisés pour illustrer une étape particulière. Le code complet est fourni dans un ensemble d'applications téléchargeables.
Qu'est-ce que P/Invoke ?
P/Invoke est l'abréviation de Platform Invoke et offre les fonctionnalités permettant d'accéder aux fonctions, structures et rappels dans les DLL non gérées. P/Invoke offre une couche de traduction permettant d'aider les développeurs en les autorisant à étendre la bibliothèque des fonctionnalités disponibles, au-delà de la bibliothèque gérée de la BCL. Le moyen le plus simple de comprendre comment utiliser P/Invoke consiste à examiner des exemples d'applications qui utilisent P/Invoke pour accomplir une tâche qui n'est pas possible avec du code géré.
Présentation des exemples
Maintenant que nous en savons un peu plus sur P/Invoke, examinons l'exemple de code qui accompagne cet article. Chaque échantillon est organisé de manière similaire et est conçu pour être ajouté et utilisé dans votre application sans trop de modifications. Toutes les déclarations de fonctions externes, structures et définitions de classe utilisées par ces fonctions résident dans une classe nommée NativeMethods. Une classe unique offre un wrapper pour tous les appels P/Invoke et est nommé de la même façon que l'échantillon (par exemple Balloon dans l'échantillon Balloon). Chaque échantillon inclut également toutes les classes et énumérations dans des fichiers de classe distincts et dans un formulaire Windows qui indique comment utiliser la fonctionnalité spécifique offerte par le code P/Invoke dans une application réelle (excepté l'échantillon Icon, utilisé dans l'échantillon Power).
Tâche 1 : Désactiver et réactiver le son
L'échantillon de désactivation/réactivation du son offre un moyen permettant de désactiver et de réactiver le son d'un ordinateur par programmation. Il utilise pour cela un certain nombre de fonctions contenues dans le fichier winmm.dll (mm signifie multimédia). Dans la mesure où il n'existe pas de fonctions permettant de désactiver ou de réactiver le son, nous pouvons les créer à l'aide des commandes xxxGetVolume et xxxSetVolume pour les divers flux sonores. Les trois flux sonores accessibles via le fichier winmm.dll sont wave, midi et aux.
Cet échantillon est l'échantillon P/Invoke le plus simple de cet article, car il ne nécessite pas la définition de structures ou de classes supplémentaires.
Figure 1
Utilisation
Après l'ajout de l'échantillon de code à votre application, l'utilisation de la fonctionnalité de désactivation et réactivation du son est très simple. Commencez par créer un objet SoundDevice en transmettant l'un des trois types de flux sonore possibles : wave, aux ou midi. Ensuite, appelez simplement les méthodes Mute et UnMute. Vous pouvez également configurer la propriété Volume de SoundDevice afin d'ajuster le volume pour chacun des flux sonores.
[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
Déclarations
La première étape consiste à identifier la DLL et les fonctions gérées que nous allons utiliser. Pour le type de flux wave, les deux fonctions sont waveOutGetVolume et waveOutSetVolume. Ces deux fonctions résident dans winmm.dll et leurs signatures non gérées sont les suivantes :
MMRESULT waveOutGetVolume(
HWAVEOUT hwo,
LPDWORD pdwVolume
);
MMRESULT waveOutSetVolume(
HWAVEOUT hwo,
DWORD dwVolume
);
Nous utilisons également des fonctions similaires pour les périphériques aux et midi.
Un autre problème, un peu plus complexe, à résoudre pour accéder à ces fonctions à partir de code géré est de déterminer comment mettre en correspondance les paramètres et les valeurs renvoyées, tels que LPDWORD et DWORD, avec les types gérés. Vous pouvez facilement faire cela en vous référant au tableau de conversion de type fourni dans la section Informations complémentaires à la fin de cet article. Ce tableau indique que les types LPDWORD et DWORD peuvent être réprésentés par le type géré UInt32. Le type HWAVEOUT est simplement un pointeur (HANDLE) vers un périphérique de sortie audio. Le tableau indique que les pointeurs sont représentés par le type géré IntPtr. En consultant la documentation ou les fichiers d'en-tête de ces API, nous découvrons que le type MMRESULT est une valeur entière, avec l'une des valeurs MMSYSERR_ comme valeur renvoyée par la fonction. Du côté géré, nous convertissons ces entiers « magiques » en constantes qui clarifient leur signification. La plupart du temps, vous devez soit convertir les valeurs renvoyées en constantes, soit – mieux encore – en une énumération afin d'identifier leur signification dans votre code géré. Dans l'échantillon de désactivation/réactivation du son, nous avons défini les valeurs de retour possibles dans l'ensemble de constantes MMSYSERR_.
Maintenant que nous disposons des informations dont nous avons besoin, nous déclarons les wrappers de la fonction P/Invoke gérée de la façon suivante :
[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
Comme nous l'avons vu précédemment, ces déclarations résident dans la classe NativeMethods.
Implémentation
Ces fonctions sont appelées à partir des méthodes GetVolume et SetVolume de la classe SoundDevice. Cette classe offre un wrapper simple d'utilisation autour des fonctions externes dans la classe NativeMethods. L'encapsulation de l'accès aux fonctions P/Invoke est une méthode recommandée, car elle offre une séparation claire entre les fonctionnalités gérées et non gérées. Vous pouvez également séparer tout le traitement d'erreur concernant le code non géré dans les classes wrapper et générer plutôt des exceptions CLR.
[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
La désactivation du son revient à mettre le volume à zéro. Pour pouvoir mettre le volume à zéro, nous devons cependant d'abord stocker le volume actuel, de façon à pouvoir réinitialiser le niveau du volume lorsque nous réactivons le son. La réactivation redéfinit simplement le volume à la valeur stockée à l'étape de désactivation. Ces deux fonctions sont détaillées dans les méthodes Mute et UnMute.
[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
Outre la désactivation et la réactivation du son, cet exemple vous permet d'ajuster le volume à l'aide du curseur, lequel appelle également les méthodes xxxGetVolume et xxxSetVolume.
Cet exemple constitue probablement l'utilisation la plus simple qui soit de P/Invoke. Excepté la déclaration des fonctions comme externes (en C#) ou partagées (en Visual Basic) et le référencement de l'espace de noms System.Runtime.InteropServices, aucune autre action n'est requise pour commencer à utiliser la nouvelle fonctionnalité. L'essentiel du travail consiste à convertir les signatures de méthodes non gérées en signatures gérées. Un scénario plus courant implique l'utilisation de structures de classes qui sont envoyées et reçues, et qui contiennent les données requises pour exécuter les fonctions.
Heureusement, les sites tels que PInvoke.net offrent un large éventail de fonctions prédéfinies en C# et Visual Basic .NET, afin de faciliter ce travail. Cependant, à des fins d'explication, cet article crée tous ces wrappers de toutes pièces.
Tâche 2 : Changement de la résolution d'affichage
L'exemple Résolution Windows utilise les fonctions EnumDisplaySettings et ChangeDisplaySettings de la bibliothèque user32.dll pour énumérer et changer par programmation les paramètres d'affichage. Il s'agit de la fonctionnalité à laquelle vous accédez généralement en cliquant avec le bouton droit sur le bureau, en sélectionnant Propriétés | Paramètres, puis en ajustant le curseur Résolution de l'écran.
Figure 2
Utilisation
Après l'instanciation de l'objet Display, vous pouvez extraire tous les types d'affichage disponibles et les stocker dans une liste DevMode en appelant la méthode GetDisplaySettings. Pour changer la résolution, transmettez simplement à la méthode ChangeSettings une structure DevMode contenant les nouveaux paramètres. Vous pouvez utiliser l'une des structures DevMode renvoyées dans l'appel GetDisplaySettings, ou créer une nouvelle 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)
Déclarations
Les fonctions Win32 EnumDisplaySettings et ChangeDisplaySettings utilisent une structure DEVMODE contenant tous les paramètres propres au périphérique d'affichage ou d'impression. Cette structure est utilisée pour renvoyer les informations de paramètre dans la fonction EnumDisplaySettings et pour changer les paramètres dans la fonction ChangeDisplaySetting. La définition non gérée de la structure DEVMODE est la suivante :
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;
Nous n'avons pas besoins des unions et des sections de condition de prétraitement de la structure DEVMODE lors du traitement des paramètres d'affichage, donc nous supposons que ((WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400)) est vrai, ainsi que les portions de l'union qui s'appliquent aux périphériques d'affichage. La définition de structure gérée qui en résulte contient uniquement les informations dont nous avons besoin.
[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
La structure est décorée avec l'attribut StructLayout et le paramètre sequential pour la propriété Layout. Pour plus d'informations concernant l'attribut StructLayout, reportez-vous à la discussion dans la section Informations complémentaires à la fin de cet article. La plupart des types membres non gérés ont été facilement convertis en types gérés à l'aide du tableau de conversion de type. Seuls les champs dmDeviceName et dmFormName posent un problème, car il s'agit de tableaux de longueur fixe qui n'ont pas de correspondance directe avec un type géré. Lorsque nous rencontrons cette situation, nous pouvons utiliser la propriété UnmanagedType de l'attribut MarshalAs afin de définir le type comme l'un des types de l'énumération UnmanagedType. Le type ByValTStr est défini comme « un tableau de longueur fixe de caractères ; le type du tableau est déterminé par le jeu de caractères de la structure contenante ». En utilisant ce type et la propriété SizeConst pour définir la taille du tableau, nous pouvons définir de manière adéquate le type du regroupeur. Notez que la propriété CharSet définie dans StructLayout détermine également le jeu de caractères de ce type. Le paramètre par défaut est Char.Ansi, mais ce paramètre peut poser des problèmes de performances sous WIN NT et vous devez utiliser le paramètre CharSet.Auto chaque fois que cela est possible afin de laisser le regroupeur déterminer le meilleur jeu de caractères à utiliser.
Implémentation
Une fois que nous avons défini les fonctions et structures, le flux de l'application est relativement simple. Nous commençons par appeler la fonction GetSettings, laquelle appelle à son tour notre déclaration P/Invoke de EnumDisplaySettings. Un compteur est transmis comme paramètre iModeNum. Nous incrémentons ce compteur jusqu'à ce que la fonction renvoie 0, ce qui indique que tous les paramètres d'affichage disponibles pour l'ordinateur ont été renvoyés et stockés dans la liste générique de 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
Pour changer le paramètre actuel, il nous suffit de transmettre à la fonction ChangeDisplaySettings une structure DevMode contenant les nouveaux paramètres. Nous avons stockés les différentes structures DevMode dans une liste, ce qui permet leur extraction facile. Nous comparons la valeur renvoyée à l'énumération ReturnCodes afin de déterminer le résultat de l'appel. Une fois encore, il est préférable de convertir les divers nombres « magiques » en énumérations ou en constantes dans le code géré afin de faciliter leur compréhension ; sinon, vous risquez de vous demander ultérieurement ce que signifie réellement le code de retour -5.
[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
Voilà tout pour cet échantillon P/Invoke. L'utilisation de structures pour transmettre des informations, plutôt qu'un grand nombre de paramètres indépendants, est le scénario le plus courant en code géré.
Pour notre dernier exemple, examinons une utilisation plus avancée de P/Invoke.
Tâche 3 : Affichage d'info-bulles
L'échantillon d'affichage d'info-bulles offre un moyen d'afficher les fenêtres de type Balloon utilisées dans Microsoft Windows. Ces bulles sont généralement utilisées pour la validation de saisie de données et s'affichent sur les champs comportant des valeurs manquantes ou incorrectes. L'exemple vous permet de changer l'alignement, l'icône, les positions et l'emplacement d'origine, afin de générer différentes bulles.
Figure 3
Utilisation
Vous pouvez créer une nouvelle bulle en créant simplement un objet Balloon, en définissant les valeurs appropriées des propriétés ParentControl, Title, Text, Icon, Position, Alignment, IsPostionAbsolute et IsStemCentered, puis en appelant la méthode Show.
Pour supprimer la bulle Balloon, appelez la méthode Dispose.
[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();
Déclarations
Cet exemple utilise quatre fonctions API Win32 (SendMessage, GetClientRect, ClientToScreen et SetWindowPos) et deux structures (TOOLINFO et RECT) pour afficher la bulle.
[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
Implémentation
Outre l'encapsulation de ces fonctions et structures d'API Win32, nous devons également créer BalloonWindow, une nouvelle classe qui hérite de la classe NativeWindow du .NET Framework. Cette classe offre une encapsulation de bas niveau d'un pointeur de fenêtre et d'une procédure de fenêtre. Nous utilisons cette classe pour déterminer si l'utilisateur a cliqué sur la bulle. Dans ce cas, un événement personnalisé (WindowClosed) est généré et provoque finalement la fermeture de la bulle. La méthode WndProc est appelée chaque fois qu'un message Windows est envoyé au pointeur de la fenêtre.
[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
Outre les constantes habituelles, cet exemple utilise également trois énumérations (BalloonAlignment, BalloonIcon et BalloonPosition) afin de nous aider à déterminer la position et l'icône utilisées pour la fenêtre. Tous les éléments de prise en charge étant définis, nous pouvons à présent créer la fenêtre de la bulle. La classe Balloon encapsule toutes les fonctionnalités requises. Elle offre les deux méthodes dont nous avons besoin pour afficher et fermer une fenêtre, ainsi qu'un certain nombre de propriétés utilisées pour décrire chaque fenêtre.
La méthode Show commence par créer une fenêtre avec la classe BalloonWindow. La méthode NativeWindow CreateHandle est essentiellement un wrapper autour de la fonction Win32 CreateWindowEx. Elle nécessite de transmettre un objet CreateParams qui définit les divers éléments de style de la nouvelle fenêtre.
[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)
L'étape suivante consiste à créer et à alimenter une structure TOOLINFO. Notez la façon dont nous utilisons la constante pour faciliter la compréhension des indicateurs.
[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
La structure BalloonWindow et TOOLINFO étant définie, nous appelons trois API Win32 afin de définir la taille et la position de la fenêtre.
[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)
Ensuite, nous envoyons les informations requises à la fenêtre afin de la convertir d'une fenêtre ordinaire en fenêtre de type bulle. L'API Win32 SendMessage est souvent utilisée pour communiquer avec une fenêtre existante et est utilisée ici pour accomplir notre tâche. Nous transmettons d'abord la structure TOOLINFO, puis nous définissons la largeur maximale de la fenêtre, et enfin nous définissons le titre. La dernière étape consiste à vider explicitement la mémoire allouée dans les méthodes AllocHGlobal et StringToHGlobalAuto en appelant la méthode FreeHGlobal.
[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)
La dernière étape consiste à afficher la fenêtre. Pour cela, le message TTM_TRACKACTIVATE est transmis à la fenêtre (contrôle d'édition) à laquelle la bulle est associée. Là encore, nous utilisons la fonction SendMessage. N'oubliez pas de libérer la mémoire allouée à l'aide de la méthode FreeHGlobal, afin d'éviter les pertes de mémoire.
[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)
Et voilà. Nous avons une bulle.
Heureusement, la fermeture de la fenêtre est beaucoup plus simple que l'ouverture. Nous appelons simplement la fonction SendMessage en transmettant la même constante TTM_TRACKACTIVATE, mais au lieu de -1, nous transmettons 0 pour le paramètre wParam.
[C#]
NativeMethods.SendMessage(_balloonWindow.Handle,
NativeMethods.TTM_TRACKACTIVATE, 0, pointer);
[Visual Basic]
NativeMethods.SendMessage(m_balloonWindow.Handle, _
NativeMethods.TTM_TRACKACTIVATE, 0, pointer)
Informations complémentaires
Attribut DLLImport La procédure élémentaire d'utilisation de P/Invoke implique toujours la création d'une fonction gérée décorée avec l'attribut DllImport. En C#, ces fonctions wrapper non gérées doivent être déclarées comme externes. En Visual Basic, elles sont déclarées comme partagées. Lorsque cette fonction est appelée, le CLR localise et charge les DLL requises, regroupe les paramètres, effectue l'appel, puis dégroupe les paramètres de sortie et les valeurs renvoyées, avant de revenir à l'application gérée.
[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
L'attribut DllImport est utilisé pour décorer une méthode gérée afin d'indiquer qu'elle utilise une bibliothèque de liaison dynamique (DLL) non gérée comme point d'entrée statique. Vous devez au minimum indiquer le nom de la DLL qui fournit le point d'entrée. Examinons chaque paramètre plus en détail.
-
Name : Nom de la DLL dans laquelle la fonction réside.
-
Entry Point : Nom ou ordinal de la fonction. Ce paramètre est utile lors de la création d'un alias de la déclaration externe afin d'éviter les conflits de noms.
-
SetLastError : Lorsque ce paramètre est défini sur true, la fonction COM appelle la fonction SetLastError avant de revenir à l'application. Cette valeur peut ensuite être lue par le code géré.
-
CharSet : Cet attribut détermine la façon dont les chaînes sont regroupées. La valeur par défaut CharSet.Auto convertit la chaîne en ANSI sur les systèmes d'exploitation Win 9X et en Unicode sous Windows NT, 2000 et XP.
-
Exact Spelling : Lorsque ce paramètre a la valeur false, le CLR est autorisé à rechercher dans la DLL des points d'entrée semblables à la valeur EntryPoint.
-
BestFitMapping : Active ou désactive un comportement d'optimisation lors de la conversion Unicode vers ANSI.
-
Calling Convention : Une valeur parmi cinq possibles. Le paramètre Winapi est le même que la convention d'appel par défaut de la plate-forme. Cela signifie que l'utilisation de Winapi et StdCall sur une plate-forme est identique.
-
Preserve Signature : Lorsque cette valeur est true, la signature de la fonction externe est identique à celle de la fonction non gérée.
-
ThrowOnUnmappableChar : Lorsque cette valeur est true, une exception est générée lorsqu'un caractère Unicode ne peut pas être représenté au format ANSI ; sinon, le caractère est converti en ? et aucune exception n'est générée.
[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
Regroupement Le regroupement est l'action de conversion des données afin qu'elles puissent être transmises et analysées correctement entre les espaces de programme gérés et non gérés. Le regroupement est effectué lors de l'exécution via le service de regroupement du CLR. Lorsque vous utilisez P/Invoke, vous devez généralement regrouper des classes et des structures, et contrôler les détails du regroupement à l'aide d'un certain nombre d'attributs de l'espace de noms System.Runtime.InteropServices.
Conversions de type Au cours du regroupement, l'une des principales étapes consiste à convertir les types non gérés en types gérés, et inversement. Le service de regroupement du CLR sait comment effectuer nombre de ces conversions, mais vous devez quand même connaître les correspondances entre les différents types lors de la conversion de la signature non gérée vers la fonction gérée. Vous pouvez utiliser le tableau de conversion suivant pour mettre en correpondance les différents types.
Tableau 1
| Type de données Windows | Type de données .NET |
| BOOL, BOOLEAN | Boolean ou Int32 |
| BSTR | String |
| BYTE | Byte |
| CHAR | Char |
| DOUBLE | Double |
| DWORD/LPDWORD | Int32 or UInt32 |
| FLOAT | Single |
| HANDLE (et tous les autres types de pointeur, tels que HFONT et HMENU) | IntPtr, UintPtr ou HandleRef |
| HRESULT | Int32 or UInt32 |
| INT | Int32 |
| LANGID | Int16 ou UInt16 |
| LCID | Int32 or UInt32 |
| LONG | Int32 |
| LPARAM | IntPtr, UintPtr ou Object |
| LPCSTR | String |
| LPCTSTR | String |
| LPCWSTR | String |
| LPSTR | String ou StringBuilder* |
| LPTSTR | String ou StringBuilder |
| LPWSTR | String ou StringBuilder |
| LPVOID | IntPtr, UintPtr ou Object |
| LRESULT | IntPtr |
| SAFEARRAY | type de tableau .NET |
| SHORT | Int16 |
| TCHAR | Char |
| UCHAR | SByte |
| UINT | Int32 or UInt32 |
| ULONG | Int32 or UInt32 |
| VARIANT | Object |
| VARIANT_BOOL | Boolean |
| WCHAR | Char |
| WORD | Int16 ou UInt16 |
| WPARAM | IntPtr, UintPtr ou Object |
Rappels
Outre l'appel de DLL non gérées à partir de code géré, vous pouvez utiliser P/Invoke pour que le code non géré rappelle du code géré. Cela permet de disposer d'un moyen asynchrone d'utiliser les ressources non gérées.
[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
Attribut StructLayout
Outre les types simples répertoriés dans le tableau de conversion de type, de nombreuses fonctions non gérées utilisent des structures afin de transmettre plus efficacement les informations. L'attribut StructLayout vous permet de spécifier comment organiser les membres de données dans les portions gérées de la mémoire, afin qu'ils puissent être facilement référencés dans les portions de mémoire non gérées. Les paramètres sont les suivants :
-
Layout : Peut être l'un des types suivants, le premier étant le plus courant.
-
LayoutKind.Sequential
-
LayoutKind.Union
-
LayoutKind.Explicit
-
Pack : Cette valeur contrôle la façon dont les champs de données sont alignés en mémoire en définissant la taille de paquet de chaque champ. Les valeurs possibles sont 1, 2, 4, 8 et 16. La valeur par défaut est de 8, sauf pour les structures non gérées dont la taille de paquet par défaut est généralement de 4.
-
Charset : Cet attribut détermine la façon dont les chaînes sont regroupées. La valeur par défaut CharSet.Auto convertit la chaîne en ANSI sur les systèmes d'exploitation Win 9X et en Unicode sous Windows NT, 2000 et XP. (Identique à l'attribut DLLImport).
-
Size : Il s'agit de la taille de la structure ou de la classe, laquelle doit être au moins égale à la somme de la taille des membres.
[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
Attribut FieldOffset
Lorsqu'une structure ou une classe utilise le paramètre de disposition explicite, chaque champ peut être décoré avec l'attribut FieldOffset. Il spécifie le décalage des champs, en octets, à partir du début de la structure ou de la classe. Il est possible de faire se chevaucher les champs, créant ainsi une structure de données d'union offrant une nouvelle valeur, en combinant les valeurs qui se chevauchent des champs distincts.
[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
Attribut MarshallAs
Il arrive que le type exact requis par la fonction non gérée n'existe pas dans le code géré et qu'il ne puisse pas être remplacé ou mis en correspondance avec un autre type géré. Dans ce cas, vous devez fournir manuellement au regroupeur les informations de type, à l'aide de l'attribut MarshallAs. L'attribut MarshallAs remplace le comportement de regroupement par défaut du service de regroupement du CLR, comme nous l'avons fait dans la structure TOOLINFO de l'exemple de bulle.
[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
L'utilisation de P/Invoke est un bon moyen d'étendre les fonctionnalités disponibles pour les développeurs. Avec P/Invoke, vous pouvez exploiter les fonctionnalités de l'API Win32 et d'autre code non géré, sans avoir à réinventer la roue dans le code géré. Cela vous permet de gagner du temps et de bénéficier de fonctionnalités robustes et testées, ce qui contribue à l'utilité globale de votre application.
Bien entendu, l'inconvénient est que l'essentiel des détails relatifs au regroupement et à la libération de la mémoire est subitement intégré au code géré lors de l'accès à des fonctions non gérées. Cela peut réduire la stabilité globale de votre application et augmenter le temps de développement ; soyez donc prudent.
Pour plus d'informations sur P/Invoke, reportez-vous à l'article Appel de DLL Win32 en C# avec P/Invoke dans le magazine MSDN en ligne.