Utilisez l'effet Glass de Vista dans vos applications WinForm

vista

Windows Vista et ses fenêtres transparentes vous fait rêver ? Apprenez à utiliser le thème Aero Glass de Windows Vista pour que vous puissiez, vous aussi, avoir de la transparence dans vos fenêtres Winform.

N'hésitez pas à commenter cet article ! Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 tirerons automatiquement parti de ces nouveautés sans que le développeur n'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:

image

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 :

 
Sélectionnez

// 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 (http://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 oeil ? 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 :

 
Sélectionnez

HRESULT DwmIsCompositionEnabled(      
    BOOL *pfEnabled );

Nous déclarons le wrapper de la fonction P/Invoke gérée de la façon suivante :

 
Sélectionnez

[DllImport("dwmapi.dll", PreserveSig = false)]
public static extern bool DwmIsCompositionEnabled();

Si vous voulez en savoir plus sur le tag PreserveSig, allez jetez un coup d'oil 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. A 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:

 
Sélectionnez

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 ou la composition est active et le cas où elle ne l'est pas:

 
Sélectionnez

//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:

 
Sélectionnez

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 à toute 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-I. Première technique

Nous allons ici utiliser la fonction DwmExtendFrameIntoClientAreadont voici la signature non gérée:

 
Sélectionnez

HRESULT DwmExtendFrameIntoClientArea(      
    HWND hWnd,     const MARGINS *pMarInset );

Et le wrapper associé:

 
Sélectionnez

[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:

 
Sélectionnez

typedef struct _MARGINS { 
int cxLeftWidth; 
int cxRightWidth; 
int cyTopHeight; 
int cyBottomHeight; 
} MARGINS, *PMARGINS;

Et sa version gérée:

 
Sélectionnez

[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'extention 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):

 
Sélectionnez

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:

 
Sélectionnez

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:

image



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:

 
Sélectionnez

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:

 
Sélectionnez

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:

image

V-II. 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:

 
Sélectionnez

HRESULT DwmEnableBlurBehindWindow(      
    HWND hWnd,     const DWM_BLURBEHIND *pBlurBehind );

Et son wrapper:

 
Sélectionnez

[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é:

 
Sélectionnez

typedef struct _DWM_BLURBEHIND { 
DWORD dwFlags; 
BOOL fEnable; 
HRGN hRgnBlur; 
BOOL fTransitionOnMaximized; 
} DWM_BLURBEHIND, *PDWM_BLURBEHIND;

Et son équivalent en code managé:

 
Sélectionnez

[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:
 
Sélectionnez

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:

 
Sélectionnez

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:

 
Sélectionnez

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:

 
Sélectionnez

//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:

 
Sélectionnez

protected override void OnPaint(PaintEventArgs e)
{
    if (WrappersDWM.DwmIsCompositionEnabled())
    {
        e.Graphics.Clear(Color.Black);
    }
    base.OnPaint(e);
}

Au final nous obtenons le résultat ci-dessous:

image

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:

image Voici un label simple avec du texte en noir à l'intérieur.

image Le même label mais avec le fond transparent. Pas génial.

image 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:

 
Sélectionnez

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:

image


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:

 
Sélectionnez

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:

image

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

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 tutorial, dans les sources, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par le forum.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2007 Florian Casabianca. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.