Lors d’un débat hautement philosophique dans un troquet parisien (mon dieu, je m’embourgeoise à une vitesse…), je discutais avec des amis (D**k et Mi**u, pour respecter leur anonymat) de la possibilité de changer la valeur de AllowTransparency au runtime.
Mais avant d’aller plus loin, remettons sur la table le fonctionnement de cette propriété. Son but est simple : activer l’opacité par pixel (per-pixel opacity) qui permet en plus de la couleur de chaque pixel d’y associer une valeur d’alpha qui permet de rendre ce dernier plus ou moins transparent.
Windows supporte deux possibilités pour faire de la transparence au niveau des fenêtres:
Les fenêtres Layered en mode System Redirected Content (SRC)
Dans ce mode, Windows crée une image pour la fenêtre et chaque appel au GDI effectué par la fenêtre ou les contrôles est redirigé vers l’image en question. Ce mode est transparent pour la fenêtre qui ne sait pas que ces ordres de dessins sont redirigés. Toutefois il n’est pas possible dans ce mode d’avoir de per-pixel opacity car les appels GDI n’en contiennent pas. Tout au plus on peut activer une transparence globale pour la fenêtre.
Dans notre cas, nous pourrions essayer ce code sur notre fenêtre WPF:
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey,
byte bAlpha, uint dwFlags);
const int GWL_ID = -12;
const int GWL_STYLE = -16;
const int GWL_EXSTYLE = -20;
const int LWA_ALPHA = 0x2;
const int WS_EX_LAYERED = 0x80000;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
WindowInteropHelper helper = new WindowInteropHelper(this);
SetWindowLong(helper.Handle, GWL_EXSTYLE,
GetWindowLong(helper.Handle, GWL_EXSTYLE) ^ WS_EX_LAYERED);
SetLayeredWindowAttributes(helper.Handle, 0, 128, LWA_ALPHA);
}
Cela marcherait très bien pour une fenêtre Windows Forms ou n’importe quelle fenêtre Win32 mais cela n’a aucun effet en WPF. En effet, WPF surveille la valeur du style de sa fenêtre (au sens Win32 du terme) et donc rejette toutes modifications telles que le SetWindowLong ci-dessus.
Il n’est donc PAS POSSIBLE d’activer le AllowTransparency une fois qu’une fenêtre a été affichée. La seule solution consisterait à réinistancier la fenêtre courante et à modifier la valeur de AllowTransparency AVANT de faire le window.Show (ou à construire une fenêtre avec AllowTransparency=true et à transférer l’arbre des éléments WPF).
Les fenêtres Layered en mode Application Provided Content (APC)
Vous l’aurez compris, WPF a fait le choix de ce mode.
Ainsi quand on active AllowTransparency=true, la fenêtre Win32 qui sert de support à WPF est créée en mode Layered APC. Dans ce mode c’est l’application qui va crée le bitmap et qui sera responsable de le mettre à jour.
Ce mode supporte le per-pixel opacity et le bitmap est donc généré au format RGBA par WPF et envoyé à l’OS comme dessin de la fenêtre. Pour ce faire il est obligatoire que le WindowStyle soit égal à None car WPF ne saurait pas dessiner la zone non-cliente (la zone de titre, les bordures ou même les effets de distorsion d’Aero) qui sont à la charge de Windows.
Il n’y a donc plus aucun WM_PAINT et tout le mécanisme Win32 de dessin est totalement zappé. Ceci explique par exemple que si l’on a un ActiveX ou un contrôle Win32 dans sa fenêtre, il n’apparaitra pas puisque lui continue à attendre les WM_PAINT et à écrire dans le device context standard (Je referai un post pour contourner ce problème).
C’est un mode performant et qui correspond bien à WPF puisque ce dernier contrôle tout et peut envoyer ses données en RGBA (ce qui est son mode de fonctionnement interne).
Hélas, comme nous l’avons vu plus haut, ce mode n’est pas dynamique et doit être fixer une fois pour toute à cause de l’inflexibilité de WPF sur ce point.
Mettre en place une region
Si toutefois, l’effet recherché est de ne pas avoir une fenêtre rectangulaire, mais de faire une fenêtre aux formes bizarres de manière dynamique, il reste la solution de définir une région:
[DllImport("gdi32.dll")]
static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2,
int cx, int cy);
[DllImport("user32.dll")]
static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
WindowInteropHelper helper = new WindowInteropHelper(this);
SetWindowRgn(helper.Handle, CreateRoundRectRgn(0, 0, 300, 300, 8, 8), true);
}
En effet, même si WPF fait sa sauce pour dessiner le contenu, notre fenêtre de part le fait qu’elle vit sous Windows doit se conformer aux fonctionnements standards et doit donc passer par le Windows Manager. Elle peut donc avoir une région définie (ce que l’on appelle aussi “Airspace”).
En conclusion, si l’on veut faire du per-pixel opacity, la seule solution reste le AllowTransparency et ce dernier ne peut être activé qu’avant l’affichage de la fenêtre.
.Net, WPF
WPF