Introduction▲
Comme vous le savez probablement tous, Windows Vista introduit avec lui un nouveau thème: Aero. Un des aspects les plus connus de ce thème est le fait que la barre de titre et les contours des fenêtres sont transparents et laissent apparaître l'arrière des fenêtres légèrement flouté.
Toutes les fenêtres des applications écrites avant ou pour Windows Vista tireront automatiquement parti de ces nouveautés sans que le développeur ait besoin d'y retoucher. Cependant vous avez peut-être remarqué que certaines applications étendent cet effet de transparence à toute la zone client et non pas seulement à la barre de titre et aux contours. C'est notamment le cas du lecteur Windows Media Player dont voici une image en mode réduit :
Pas mal, non ? Et bien sachez qu'il vous est possible d'utiliser l'API qui se cache derrière tout ça pour que vous puissiez, vous aussi, étendre la transparence à toute la zone cliente de vos applications WinForm. C'est ce que nous allons voir au travers de cet article.
I. Desktop Window Manager▲
Le Desktop Window Manager (DWM) est la nouvelle interface qui contrôle l'affichage et le rafraîchissement des fenêtres en cours d'exécution sur le bureau Windows Vista. Le DWM contrôle la manière dont les fenêtres interagissent avec le moteur de composition de bureau. C'est grâce à lui que vous avez des fonctionnalités comme la transparence, le flip 3D, les miniatures des applications lancées, etc. Pour plus d'informations sur DWM, je vous invite à suivre les différents liens présents à la fin de cet article.
Toutes les applications que vous avez déjà programmées tirent profit du DWM sans avoir à subir de modification (comme la transparence des rebords des fenêtres). Toutefois, si vous souhaitez contrôler ou accéder aux fonctionnalités de DWM, vous devrez appeler les interfaces se trouvant dans dwmapi.dll (l'interface publique de DWM). C'est précisément ce que nous allons devoir faire pour étendre l'effet de transparence à la zone cliente d'une fenêtre.
II. Avant de commencer▲
Pour pouvoir utiliser l'effet de transparence, votre programme devra vérifier certaines conditions concernant son environnement.
Il faudra tout d'abord vérifier que l'application s'exécute bien sous Windows Vista. En effet, vouloir utiliser l'API de DWM sous Windows XP, par exemple, risque de vous poser quelques problèmes.
Vous pouvez donc par exemple insérer ce bout de code dans la fonction main de votre programme :
// on vérifie qu'on se trouve bien au moins sous Vista
if
(
Environment.
OSVersion.
Version.
Major <
6
)
{
MessageBox.
Show
(
"Windows Vista est requis."
,
"Erreur"
);
return
;
}
Ainsi, l'application se fermera si elle ne se trouve pas sous Vista. Cela peut être ennuyeux si vous souhaitez tout de même la faire tourner sous Windows XP par exemple. Pour contourner ce problème, vous pouvez, par exemple, faire la vérification de la version de l'OS non pas dans la fonction main, mais aux endroits où vous utilisez l'API de DWM. Vous activerez ainsi la transparence seulement si vous êtes sous Vista et pas dans les autres cas.
Un autre point à vérifier est l'activation ou non d'Aero. En effet, un utilisateur sous Windows Vista peut ne pas l'avoir activé (ou sa configuration matérielle ne lui permet pas de le faire). Cette vérification peut se faire en appelant la fonction DwmIsCompositionEnabled de l'API de DWM qui permet obtenir l'état de composition DWM du bureau. Nous verrons plus loin comment utiliser cette fonction.
III. L'API de DWM▲
C'est maintenant que les problèmes commencent.En effet, nous souhaitons utiliser la dll dwmapi.dll dans notre programme C#. Or cette dernière n'a pas été écrite en .NET, elle est en code dit non géré (ou non managé). Pour pallier ce problème, on utilise le mécanisme de : P/Invoke (Platform Invoke). Le site MSDN nous donne une rapide définition de ce mécanisme.
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 (Base Class Library) du .NET Framework.
Pour plus d'informations sur ce mécanisme, je vous invite à aller consulter cet article (https://morpheus.developpez.com/dlldotnet/) de Thomas Lebrun ou encore celui-ci (http://www.microsoft.com/france/msdn/vcsharp/Utilisez-Pinvoke.mspx ) sur le site MSDN.
Vous y avez jeté un œil ? Parfait, nous allons pouvoir continuer. Vous avez donc compris que nous allons devoir créer des wrappers pour les fonctions et structures requises par le DWM afin que nous puissions les appeler à partir de notre programme C#. Une description de l'API de DWM est disponible sur MSDN à cette adresse: http://msdn2.microsoft.com/en-us/library/aa969540.aspx.
IV. Vérifier l'activation de la composition▲
La première chose est donc de vérifier l'activation de la composition sous Vista en utilisant la fonction DwmIsCompositionEnabled dont voici la signature non gérée :
HRESULT DwmIsCompositionEnabled(
BOOL *pfEnabled );
Nous déclarons le wrapper de la fonction P/Invoke gérée de la façon suivante :
[DllImport(
"dwmapi.dll"
, PreserveSig = false)]
public
static
extern
bool
DwmIsCompositionEnabled
(
);
Si vous voulez en savoir plus sur le tag PreserveSig, allez jeter un coup d'œil sur cette page MSDN. Sinon, sachez simplement qu'il permet de transformer la valeur de retour HRESULT directement en exception si besoin.
Ce wrapper n'est que le premier d'une longue liste. Pour une meilleure lisibilité, vous devriez les déclarer dans une classe à part. Vous pouvez vous inspirer des sources données en exemple. À l'intérieur se trouve la classe WrappersDWM qui référence tous les wrappers déclarés.
N'oubliez pas de référencer l'espace de noms System.Runtime.InteropServices en utilisant la directive suivante :
using
System.
Runtime.
InteropServices;
Une fois cela terminé, vous serez en mesure d'appeler la fonctionDwmIsCompositionEnabled comme s'il s'agissait d'une fonction managée.
Voici un exemple de code que vous pouvez utiliser pour traiter le cas où la composition est active et le cas où elle ne l'est pas :
//on vérifie si la composition est activée ou non
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
//on peut activer la transparence
}
else
{
//pas de transparence ici !
}
Nous sommes maintenant capables de savoir si la composition est activée ou non. Mais il reste un petit problème. En effet, l'utilisateur peut à tout moment désactiver Aero. Votre application doit être capable de réagir immédiatement (si besoin) à cette modification. Heureusement, lorsque l'état de composition du bureau est modifié, un message système WM_DWMCOMPOSITIONCHANGED est diffusé. Nous pouvons donc le récupérer et effectuer les traitements nécessaires si besoin. Par contre, ce message ne nous informe pas sur l'état (activé ou non) de la composition. Vous devrez refaire un appel à la méthode DwmIsCompositionEnabled pour le déterminer.
Pour récupérer les messages Windows nous devons redéfinir la méthode WndProc. Cette technique n'est pas propre à Vista, elle existe depuis la version 1.0 du FrameWork. Si vous souhaitez plus d'informations sur le sujet rendez-vous à l'adresse suivante: http://msdn2.microsoft.com/fr-fr/library/system.windows.forms.control.wndproc(vs.80).aspx.
Voici à quoi pourrait ressembler le code de cette fonction :
protected
override
void
WndProc
(
ref
Message msg)
{
base
.
WndProc
(
ref
msg);
const
int
WM_DWMCOMPOSITIONCHANGED =
0x031E
;
//valeur associée au message
switch
(
msg.
Msg)
{
case
WM_DWMCOMPOSITIONCHANGED:
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
MessageBox.
Show
(
"Composition activée"
);
//on peut activer la transparence
}
else
{
MessageBox.
Show
(
"Composition non activée"
);
//pas de transparence ici !
}
break
;
}
}
Il existe d'autres messages système liés à DWM qu'il peut être intéressant d'écouter, mais que nous ne détaillerons pas ici :
- WM_DWMCOLORIZATIONCOLORCHANGED est envoyé lorsque la couleur ou l'opacité de l'effet de verre est modifiée. Les paramètres vous informent de la nouvelle couleur et de la nouvelle opacité ;
- WM_DWMNCRENDERINGCHANGED est envoyé lorsque le rendu DWM change sur la zone non client ;
- WM_DWMWINDOWMAXIMIZEDCHANGE est envoyé lorsqu'une fenêtre compositée DWM est agrandie ou minimisée. Par exemple, la barre des tâches réagit à cet évènement en devenant opaque.
V. Et la transparence fut▲
Nous pouvons maintenant entrer dans le vif du sujet. Pour rendre transparente une fenêtre, il existe deux techniques :
la première, la plus facile, utilise la fonction DwmExtendFrameIntoClientArea et permet d'étendre les bordures transparentes de la fenêtre à tout ou une partie de la zone client ;
la deuxième technique, un peu plus complexe, mais qui permet plus de souplesse, utilise la fonction DwmEnableBlurBehindWindow et permet d'afficher une zone transparente de n'importe quelle forme à l'intérieur de la zone cliente de la fenêtre.
V-A. Première technique▲
Nous allons ici utiliser la fonction DwmExtendFrameIntoClientAreadont voici la signature non gérée :
HRESULT DwmExtendFrameIntoClientArea(
HWND hWnd, const MARGINS *pMarInset );
Et le wrapper associé :
[DllImport(
"dwmapi.dll"
, PreserveSig = false)]
public
static
extern
void
DwmExtendFrameIntoClientArea
(
IntPtr hWnd,
ref
MARGINS pMargins);
Cette fonction prend en paramètre le handle de la fenêtre dont les bords doivent être étendus ainsi qu'une structure MARGINS qui décrit comment les quatre marges de la fenêtre doivent être étendues.
Voici la version non gérée de cette structure :
typedef struct _MARGINS {
int cxLeftWidth;
int cxRightWidth;
int cyTopHeight;
int cyBottomHeight;
} MARGINS, *PMARGINS;
Et sa version gérée :
[StructLayout(LayoutKind.Sequential)]
public
struct
MARGINS
{
public
int
cxLeftWidth,
cxRightWidth,
cyTopHeight,
cyBottomHeight;
public
MARGINS
(
int
left,
int
right,
int
top,
int
bottom)
{
cxLeftWidth =
left;
cyTopHeight =
top;
cxRightWidth =
right;
cyBottomHeight =
bottom;
}
}
La première chose à faire est donc de construire un objet MARGINS qui permet d'indiquer pour chaque marge la distance (en pixels) à laquelle elle doit s'étendre à l'intérieur de la zone cliente. Une valeur de -1 indique que la marge doit s'étendre sur toute la zone cliente. Pour annuler l'extension de la marge, il suffit de redéfinir un objet MARGINS avec des valeurs à 0.
Il faut ensuite appeler la méthode DwmExtendFrameIntoClientAreaen lui passant le handle de la fenêtre et l'objet MARGINS.
Voici un exemple de code afin d'étendre la marge gauche à toute la zone cliente (c'est-à-dire étendre la transparence à toute la fenêtre) :
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
//on peut activer la transparence
glassMarges =
new
WrappersDWM.
MARGINS
(-
1
,
0
,
0
,
0
);
WrappersDWM.
DwmExtendFrameIntoClientArea
(
this
.
Handle,
ref
glassMarges);
this
.
Invalidate
(
);
//pour forcer le repaint
}
Notez l'appel à la méthode Invalidate qui permet de repeindre la fenêtre. En effet, la dernière chose à faire est de peindre les zones que l'on souhaite voir transparentes avec un brush noir. Pour cela on redéfinit la méthode OnPaint de la fenêtre.
Le code ci-dessous permet de peindre l'ensemble de la zone cliente en noir :
protected
override
void
OnPaint
(
PaintEventArgs e)
{
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
e.
Graphics.
FillRectangle
(
Brushes.
Black,
this
.
ClientRectangle);
}
base
.
OnPaint
(
e);
}
Tout cela nous permet d'obtenir le résultat suivant :
Prenons un deuxième exemple où l'on ne souhaite rendre transparent qu'un bandeau de 100 pixels de hauteur en haut de la fenêtre.
Il nous faut construire l'objet MARGINS de cette façon :
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
//on peut activer la transparence
glassMarges =
new
WrappersDWM.
MARGINS
(
0
,
0
,
100
,
0
);
WrappersDWM.
DwmExtendFrameIntoClientArea
(
this
.
Handle,
ref
glassMarges);
this
.
Invalidate
(
);
//pour forcer le repaint
}
Puis modifier la fonction OnPaint pour ne peindre en noir que ce bandeau :
protected
override
void
OnPaint
(
PaintEventArgs e)
{
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
e.
Graphics.
FillRectangle
(
Brushes.
Black,
Rectangle.
FromLTRB
(
0
,
0
,
this
.
ClientRectangle.
Width,
glassMarges.
cyTopHeight));
}
base
.
OnPaint
(
e);
}
Cela nous permet d'obtenir le résultat ci-dessous :
V-B. Deuxième technique▲
La plupart des personnes qui souhaitent ajouter de la transparence à leur fenêtre vont probablement se satisfaire de la première méthode. Il existe néanmoins une autre méthode permettant d'avoir plus de contrôle sur la façon dont l'effet de transparence est construit. Il ne s'agit pas ici d'utiliser les bordures de la fenêtre pour les étendre, mais d'indiquer une région de la fenêtre à rendre floue.
Nous allons utiliser ici la fonction DwmEnableBlurBehindWindow dont voici la signature non gérée :
HRESULT DwmEnableBlurBehindWindow(
HWND hWnd, const DWM_BLURBEHIND *pBlurBehind );
Et son wrapper :
[DllImport(
"dwmapi.dll"
, PreserveSig = false)]
public
static
extern
void
DwmEnableBlurBehindWindow
(
IntPtr hWnd,
ref
DWM_BLURBEHIND pBlurBehind);
Cette fonction ressemble à la fonction DwmExtendFrameIntoClientArea sauf que la structure à passer en paramètre est différente. En voici d'ailleurs la signature en code non managé :
typedef struct _DWM_BLURBEHIND {
DWORD dwFlags;
BOOL fEnable;
HRGN hRgnBlur;
BOOL fTransitionOnMaximized;
} DWM_BLURBEHIND, *PDWM_BLURBEHIND;
Et son équivalent en code managé :
[StructLayout(LayoutKind.Sequential)]
public
struct
DWM_BLURBEHIND
{
public
uint
dwFlags;
[MarshalAs(UnmanagedType.Bool)]
public
bool
fEnable;
public
IntPtr hRegionBlur;
[MarshalAs(UnmanagedType.Bool)]
public
bool
fTransitionOnMaximized;
public
const
uint
DWM_BB_ENABLE =
0x00000001
;
public
const
uint
DWM_BB_BLURREGION =
0x00000002
;
public
const
uint
DWM_BB_TRANSITIONONMAXIMIZED =
0x00000004
;
}
Nous allons voir un peu plus loin à quoi vont servir les constantes déclarées.
Voici un descriptif des différents champs :
- fEnable: booléen à mettre à vrai si l'on veut appliquer la transparence ;
- hRegionBlur: représente la région à rendre floue ;
- fTransitionOnMaximized: indique si l'effet de transparence doit devenir opaque quand une fenêtre (n'importe laquelle) du bureau est maximisée. Pour mieux comprendre, pensez à la barre des tâches de Vista: en temps normal elle est transparente, mais si on maximise une fenêtre elle devient opaque ;
- dwFlags: ce champ est une combinaison de constantes permettant d'indiquer quels membres (parmi les trois précédents) ont été définis. Les constantes à utiliser sont celles déclarées dans le code de la structure DWM_BLURBEHIND plus haut. Ainsi, si l'on renseigne les trois champs ci-dessus, il faudra remplir ce membre de cette façon :
dwFlags =
WrappersDWM.
DWM_BLURBEHIND.
DWM_BB_ENABLE |
WrappersDWM.
DWM_BLURBEHIND.
DWM_BB_BLURREGION |
WrappersDWM.
DWM_BLURBEHIND.
DWM_BB_TRANSITIONONMAXIMIZED;
Pour construire une région (objet de type Region), on passe généralement par un objet GraphicsPath. N'oubliez pas d'importer l'espace de nom System.Drawing.Drawing2D.
Voici un exemple de code où l'on construire une région de forme ovale :
GraphicsPath gp =
new
GraphicsPath
(
);
gp.
AddEllipse
(
this
.
Width /
2
-
150
,
30
,
300
,
150
);
Region regionBlur =
new
Region
(
gp);
Pour illustrer cette technique, nous allons construire une fenêtre un peu spéciale. Elle ne sera pas rectangulaire comme les fenêtres par défaut, mais ovale et sans bordure ni barre de titre. Bien sûr on lui appliquera l'effet de transparence.
Pour mémoire, définir une forme particulière à une fenêtre s'effectue en lui assignant une nouvelle valeur à sa propriété Region (de type Region).
Ainsi, rendre une fenêtre de forme elliptique se fera de cette manière :
GraphicsPath gp =
new
GraphicsPath
(
);
gp.
AddEllipse
(
this
.
Width /
2
-
150
,
30
,
300
,
150
);
Region regionBlur =
new
Region
(
gp);
this
.
Region =
regionBlur;
Mettons maintenant tout ceci en pratique. Voici le code permettant de créer une fenêtre ovale et transparente :
//on vérifie si la composition est activée ou non
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
//on peut activer la transparence
using
(
Graphics gc =
CreateGraphics
(
))
{
GraphicsPath gp =
new
GraphicsPath
(
);
gp.
AddEllipse
(
this
.
Width /
2
-
150
,
30
,
300
,
150
);
Region regionBlur =
new
Region
(
gp);
//on rend la fenêtre ovale en utilisant la même Region
this
.
Region =
regionBlur;
WrappersDWM.
DWM_BLURBEHIND bbh =
new
WrappersDWM.
DWM_BLURBEHIND
(
);
bbh.
dwFlags =
WrappersDWM.
DWM_BLURBEHIND.
DWM_BB_ENABLE |
WrappersDWM.
DWM_BLURBEHIND.
DWM_BB_BLURREGION |
WrappersDWM.
DWM_BLURBEHIND.
DWM_BB_TRANSITIONONMAXIMIZED;
bbh.
fEnable =
true
;
bbh.
hRegionBlur =
regionBlur.
GetHrgn
(
gc);
bbh.
fTransitionOnMaximized =
false
;
WrappersDWM.
DwmEnableBlurBehindWindow
(
this
.
Handle,
ref
bbh);
}
this
.
Invalidate
(
);
//pour forcer le repaint
}
else
{
MessageBox.
Show
(
"Composition non activée"
);
//pas de transparence ici !
}
Bien sûr, il vous faut toujours peindre le fond de la fenêtre en noir dans la méthode OnPaint :
protected
override
void
OnPaint
(
PaintEventArgs e)
{
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
e.
Graphics.
Clear
(
Color.
Black);
}
base
.
OnPaint
(
e);
}
Finalement nous obtenons le résultat ci-dessous :
VI. Dessiner sur la transparence▲
Maintenant que votre fenêtre est transparente, vous voudriez sans doute la garnir et y ajouter du texte.
Vous allez malheureusement être confronté à quelques petits soucis :
Voici un label simple avec du texte en noir à l'intérieur.
Le même label, mais avec le fond transparent. Pas génial.
Encore le même, mais avec un fond noir (c'est la couleur qui détermine les zones transparentes). Ce n'est pas encore ça.
Une solution ici est de ne pas utiliser de label, mais de peindre directement le texte que l'on souhaite afficher en utilisant les classes du GDI+. Par contre, n'utilisez pas la méthode DrawString de la classe Graphics. Il faut passer par l'intermédiaire d'un objet GraphiquePath.
Voici un exemple de code que vous pouvez mettre dans méthode OnPaint :
Graphics g =
this
.
CreateGraphics
(
);
GraphicsPath blackfont =
new
GraphicsPath
(
);
SolidBrush brsh =
new
SolidBrush
(
Color.
White);
blackfont.
AddString
(
"Du texte sur de la transparence"
,
new
FontFamily
(
"Tahoma"
),
(
int
)FontStyle.
Regular,
26
,
new
Point
(
10
,
10
),
StringFormat.
GenericDefault);
g.
SmoothingMode =
System.
Drawing.
Drawing2D.
SmoothingMode.
HighQuality ;
g.
FillPath
(
brsh,
blackfont);
Et le résultat obtenu :
Il faut utiliser la même technique pour les images (pictureBox déconseillée). Vous pouvez par contre ici simplement utiliser la fonction DrawImage de la classe Graphics.
Une simple ligne ajoutée à la méthode OnPaint suffit pour afficher une image qui se trouve en ressource :
protected
override
void
OnPaint
(
PaintEventArgs e)
{
if
(
WrappersDWM.
DwmIsCompositionEnabled
(
))
{
e.
Graphics.
FillRectangle
(
Brushes.
Black,
this
.
ClientRectangle);
//affichage de l'image
e.
Graphics.
DrawImage
(
global
::
GlassVista.
Properties.
Resources.
logo,
new
Point
(
10
,
60
));
}
base
.
OnPaint
(
e);
}
Et le résultat obtenu :
Conclusion▲
Notre petit tour dans le monde de la transparence touche à sa fin. Nous avons vu les bases pour la création de fenêtres WinForm avec l'effet Aero Glass de Windows Vista. N'hésitez pas à consulter les différents sites Web référencés ci-dessous pour plus d'informations.
Liens▲
Windows Vista Display Driver Model
Créez des effets spéciaux avec le Gestionnaire de fenêtres du Bureau
Utilisation de P/Invoke pour appeler des API non gérées à partir de vos classes gérées
Sources▲
Téléchargez les sources de la solution donnée en exemple.
Remerciements▲
J'adresse ici tous mes remerciements à l'équipe de rédaction de « developpez.com » pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.
Contact▲
Si vous constatez une erreur dans le tutoriel, dans les sources, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par le forum.