Gestion de la barre des tâches Aero dans une application MDI

| Bruno Guérangé | ero-sennin | Franck SORIANO |

Cet article présente la méthode pour gérer la barre des tâches par document sous Windows 7 avec le thème Aero actif.

Commentez cet article : Commentez ♪

- Titre : Gestion de la barre des tâches Aero dans une application MDI

- Auteurs : Bruno Guérangé, ero-sennin, Franck SORIANO

- Parution : 20 juin 2011

- Licence : Copyright ® 2011 Bruno Guérangé. 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.

Sommaire

- I. Introduction

- II. Le principe

- II-A. Préparation du projet

- II-B. Création des boutons

- II-C. Gérer les icônes et prévisualisations

- II-C-1. Création d'un 'cache'

- II-C-2. Donner une icône pour la barre des tâches

- II-C-3. Donner un aperçu de la fiche

- II-C-4. Gérer le titre de la fenêtre

- II-C-5. Changer l'icône représentant la fiche

- II-D. Gérer les actions sur les icônes

- III. Conclusion

I - Introduction

Depuis l'arrivée de Windows Vista le style Aero permet d'obtenir des icônes des applications lancées en survolant la barre des tâches.
Depuis Windows 7, une interface existe pour gérer ces nouvelles fonctionnalités.

La gestion des icônes est presque automatique pour toutes les applications même celles développées avant l'existence de Windows Vista. Mais dans le cas d'une application MDI, Windows affiche seulement une icône pour l'application entière et non pas une icône par document ouvert.
L'affichage par défaut est le suivant :

Or, dans le cas d'une application MDI, nous aimerions avoir une icône par document ouvert et pouvoir les survoler comme on peut le faire avec Excel ou Internet Explorer.
Nous allons donc voir les étapes pour arriver à :

II - Le principe

La solution se découpe en deux parties distinctes :
- création des boutons dans la barre des tâches ;
- fournir à Windows des icônes et aperçus.

II-A - Préparation du projet

Dans l'exemple suivant, nous allons partir du modèle de projet MDI donné dans Delphi. « Sélectionner Fichier->Nouveau->Autre… » Dans la rubrique « projets Delphi », prendre « Application MDI ».
Ajouter la variable globale msgTaskbarButtonCreated : cardinal; après la définition de MainForm.
Ajouter en fin de l'unité Main.pas le code pour obtenir le numéro de message reçu après la création du bouton dans la barre des tâches :

initialization
// On commence par demander à Windows quel est l'identifiant du message
// TaskbarButtonCreated. Ce dernier sera envoyé par Windows à l'application
// lorsque son bouton aura été créé dans la barre des tâches.
msgTaskbarButtonCreated := RegisterWindowMessage('TaskbarButtonCreated');
end.

Ajouter dans la définition de la fiche la surcharge de la méthode WndProc et la variable TaskBar qui contiendra une instance de l'interface ITaskBarList3 :

TMainForm = class(TForm)
{...}
protected
procedure WndProc(var Message: TMessage); override;
public
{ Déclarations publiques }
TaskBar : ITaskBarList3;
end;
{...}

Les images sont fournies à Windows en réponse aux messages WM_DWMSENDICONICTHUMBNAIL et WM_DWMSENDICONICLIVEPREVIEWBITMAP. L'application doit y répondre en utilisant les fonctions DwmSetIconicThumbnail et DwmSetIconicLivePreviewBitmap réciproquement.

procedure TMainForm.WndProc(var Message: TMessage);
begin
// On regarde si le message reçu correspond au message TaskbarButtonCreated
if Message.Msg = msgTaskbarButtonCreated Then
begin
// On a reçu le message qu'on attendait, à présent on peut demander l'interface
// ITaskBarList3
TaskBar := CreateComObject(CLSID_TaskbarList) as ITaskBarList3;
end else
inherited WndProc(Message);
end;

Dans le modèle de projet donné dans Delphi, le source du projet n'active pas la gestion de la barre des tâches, on ne reçoit alors jamais le message msgTaskbarButtonCreated. Il faut ouvrir le source du projet et ajouter la ligne suivante :

program Mdiapp;

uses
Forms,
MAIN in 'MAIN.PAS' {MainForm},
CHILDWIN in 'CHILDWIN.PAS' {MDIChild},
about in 'about.pas' {AboutBox};

{$R *.RES}

begin
Application.Initialize;
Application.MainFormOnTaskbar := True;     // Ajouter cette ligne
Application.CreateForm(TMainForm, MainForm);
Application.CreateForm(TAboutBox, AboutBox);
Application.Run;
end.

II-B - Création des boutons

Les boutons sont créés à l'aide de l'interface ITaskBarList3. Vous avez une présentation de cette interface dans l'article de Franck Soriano sur l'utilisation de la barre des tâches de Windows avec Delphi 2010.

Au lancement d'une application, si aucun code spécifique n'est appelé, Windows crée automatiquement un bouton dans la barre des tâches. Ce bouton est lié au handle de la fenêtre principale.
Pour avoir un bouton par document, il faut créer spécifiquement un bouton pour chaque document ouvert et le détruire pour chaque document fermé.

L'ajout d'un bouton est effectué par l'appel de la méthode ITaskbarList3.RegisterTab, il est ensuite affiché à la position voulue via la méthode ITaskbarList3.SetTabOrder.
Cet appel sera effectué dans l'évènement OnCreate de la fiche MDI fille.

procedure TMDIChild.FormCreate(Sender: TObject);
begin
// On demande la création du bouton dans la barre des tâches
MainForm.TaskBar.RegisterTab(Handle,MainForm.Handle);
// On le place en fin de liste
MainForm.TaskBar.SetTabOrder(Handle,0);
end;

Notez que le bouton automatiquement créé par Windows au lancement de l'application est automatiquement supprimé dès le premier appel de RegisterTab et SetTabOrder. Il n'est pas utile de le supprimer explicitement.

La suppression d'un bouton est effectuée par l'appel de la méthode ITaskbarList3.UnregisterTab.
Cet appel sera effectué dans le OnClose. Sachant que dans le cas d'une application MDI le OnClose demande la destruction de la fiche, si vous conservez la fiche en mémoire et que vous voulez conserver le bouton dans la barre des tâches, c'est dans OnDestroy qu'il faudra appeler UnregisterTab.

procedure TMDIChild.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// On désenregistre le bouton de la barre des tâches.
MainForm.TaskBar.UnregisterTab(Handle);

Action := caFree;
end;

Notez que lorsque le dernier bouton est détruit, Windows recrée automatiquement un bouton lié à la fenêtre principale ; nous n'avons donc pas à nous soucier de ce point.

Il est également possible de sélectionner par défaut l'un des boutons de la barre de tâches représentant le document actif de l'application.
Ceci est effectué par l'appel de ITaskbarList3.SetTabActive, cet appel étant effectué dans l'évènement OnActivate de la fiche MDIChild :

procedure TMDIChild.FormActivate(Sender: TObject);
begin
// Quand la fiche fille devient active, l'icône de la fiche dans la barre
// des tâches devient celle sélectionnée par défaut
MainForm.TaskBar.SetTabActive(Handle,MainForm.Handle,0);
end;

À l'exécution les boutons sont bien créés… mais vides.

II-C - Gérer les icônes et prévisualisations

Pour fournir à Windows des icônes et des prévisualisations, il faut gérer les messages WM_DWMSENDICONICTHUMBNAIL et WM_DWMSENDICONICLIVEPREVIEWBITMAP. L'application doit y répondre en utilisant les fonctions DwmSetIconicThumbnail et DwmSetIconicLivePreviewBitmap.
Or c'est là que le problème se corse : le DVM (Desktop Windows Manager) ne dialogue qu'avec les fenêtres « Top level », c'est-à-dire les fenêtres sans parent. Les fenêtres MDIChild ne sont pas concernées, car elles ont pour parent commun une fenêtre de classe MDIClient. La notion de parent est ici celle des API Windows, la propriété MDIChild.Parent vaut bien Nil. Le handle MDIChild.Handle a lui pour fenêtre parente la zone client MDI de la fiche principale.

II-C-1 - Création d'un 'cache'

Pour contourner ce problème, nous allons créer une fiche vide pour chaque fiche MDIChild ouverte. Cette fiche « cache » ne sera jamais visible, mais permettra de gérer correctement tous les messages Windows liés à la barre des tâches.

Dans Delphi, ajoutez une nouvelle fiche nommée FormCache dans une unité UFormCache. Une fois la fiche créée, supprimez sa création automatique dans les options de projet et supprimez la ligne FormCache:TFormCache. Nous allons déporter dans cette fiche toute la gestion de la barre des tâches.

En plus de ne pas être une fiche de type MDIChild, une fiche doit avoir les attributs DWMWA_HAS_ICONIC_BITMAP et DWMWA_FORCE_ICONIC_REPRESENTATION afin de recevoir les messages concernés. Nous allons donc ajouter les attributs à la création de la fiche :

procedure TFormCache.FormCreate(Sender: TObject);
Var
fForceIconic : Bool;
fHasIconicBitmap : BOOL;
begin
// À la création de la fiche on change le style de la fenêtre
// pour prévenir Windows qu'il doit nous demander une icône
// et un aperçu.
// Windows n'est pas capable de le faire de lui-même sur une fiche
// enfant MDI.
fForceIconic := True;
fHasIconicBitmap := True;
DwmSetWindowAttribute(
Handle,
DWMWA_HAS_ICONIC_BITMAP,
@fHasIconicBitmap,
sizeof(fHasIconicBitmap));

DwmSetWindowAttribute(
Handle,
DWMWA_FORCE_ICONIC_REPRESENTATION,
@fForceIconic,
sizeof(fForceIconic));
end;

Pour créer la fiche « cache », nous allons ajouter une méthode de classe permettant de créer une fiche de cache liée à une fiche MDIChild.
Au passage nous créons le bouton lié au document dans la barre des tâches, notez que c'est bien le handle de la fiche « cache » qui est passé à Windows et non pas le Handle de la fiche MDIChild.

TFormCache = class(TForm)
{...}
public
{ Déclarations publiques }
Class Function CreationFicheCache(FicheMDIFille:TForm):TFormCache;
end;

{...}

class function TFormCache.CreationFicheCache(FicheMDIFille: TForm): TFormCache;
begin
if MainForm.TaskBar=Nil then
Begin
Result := Nil;
Exit;
End;

// Création de la fiche "cache" et mise à jour des références
Result := TFormCache.Create(FicheMDIFille);
Result.MDIFille := FicheMDIFille;
Result.Caption := FicheMDIFille.Caption;

// On demande la création du bouton dans la barre des tâches
MainForm.TaskBar.RegisterTab(Result.Handle,MainForm.Handle);
// On le place en fin de liste
MainForm.TaskBar.SetTabOrder(Result.Handle,0);
end;

Du coup dans le OnCreate de la fiche fille on ne crée que la fiche « cache », le cache se chargera du bouton.

procedure TMDIChild.FormCreate(Sender: TObject);
begin
Cache := TFormCache.CreationFicheCache(Self);
end;

Comme la fiche MDIChild est donnée comme propriétaire de la fiche « cache », la destruction de la fiche fille entrainera la destruction automatique de la fiche « Cache ».
Le OnClose de la fiche MDIChild redevient classique :

procedure TMDIChild.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;

Tandis que le retrait du bouton est effectué dans le OnDestroy de la fiche « cache » :

procedure TFormCache.FormDestroy(Sender: TObject);
begin
// On désenregistre le bouton de la barre des tâches.
MainForm.TaskBar.UnregisterTab(Handle);
end;

La gestion du bouton actif sera aussi effectuée dans la fiche « cache », la fille MDIChild demandant à la fiche « Cache » de devenir le bouton actif.

TFormCache = class(TForm)
{...}
public
{ Déclarations publiques }
Class Function CreationFicheCache(FicheMDIFille:TForm):TFormCache;
end;

{...}

procedure TFormCache.DoActivate;
begin
// Quand la fiche fille devient active, l'icône de la fiche dans la barre
// des tâches devient celle sélectionnée par défaut
MainForm.TaskBar.SetTabActive(Handle,MainForm.Handle,0);
end;

procedure TMDIChild.FormActivate(Sender: TObject);
begin
if Cache<>Nil then
Cache.DoActivate;
end;

Il ne reste plus qu'à répondre aux messages de Windows pour fournir les images. Si on ne le fait pas, Windows affiche une fenêtre vide avec le sablier en attendant une hypothétique réponse de l'application :

II-C-2 - Donner une icône pour la barre des tâches

Pour ajouter une icône dans la barre des boutons créée pour la barre des tâches, il faut répondre au message WM_DWMSENDICONICTHUMBNAIL avec la fonction DwmSetIconicThumbnail.

Dans la fiche « cache », nous allons donc surcharger la méthode WndProc afin de répondre aux messages nous intéressant.
Le message WM_DWMSENDICONICTHUMBNAIL donne la taille maximale de l'icône qu'il faut renvoyer, si l'une des dimensions est supérieure l'image sera refusée.

Il faut donner à la fonction DwmSetIconicThumbnail un DIB contenant une image. Les dimensions maximales de cette image sont données en paramètre du message : Message.LParamHi donne la largeur maximale et Message.LParamLo donne la hauteur maximale. L'image doit être de 32 bits par pixel. Le contenu lui-même est libre, vous pouvez y mettre une miniature de la fiche ou une photo de vos vacances ;).
Dans l'exemple nous allons créer une image réduite de la fenêtre, comportement classique sous Windows.
Pour ce faire nous demandons à la fiche de se dessiner dans un bitmap de la même taille.

// On reçoit une demande d'icône ou de prévisualisation taille réelle.
// Dans les deux cas il capture la copie de la fiche
BMP := TBitMap.Create;
Try
// Le bitmap doit être de type DIB et non DDB le type par défaut
BMP.HandleType := bmDIB;
// On ajuste le bitmap à la taille de la zone client
BMP.Width := MDIFille.ClientWidth;
BMP.Height := MDIFille.ClientHeight;
// Et on demande simplement à la fiche de se dessiner dedans
// Cette méthode a l'avantage de fonctionner même si la fiche
// est cachée.
MDIFille.PaintTo(BMP.Canvas,0,0);

{...}

Finally
Bmp.Free;
End;

Puis nous allons copier à l'échelle cette image dans une autre. Cette deuxième image sera aux dimensions maximales autorisées afin d'être passée à la fonction DwmSetIconicThumbnail.

if Message.Msg = WM_DWMSENDICONICTHUMBNAIL then
Begin
// Dans le cas d'une icône il faut d'abord la réduire à
// la taille maximale acceptée par Windows. Cette taille
// est donnée en paramètre de message
ICO := TBitMap.Create;
Try
ICO.HandleType := bmDIB;
// On teste le coeff de réduction pour garder la proportion de l'image
if (BMP.Width/Message.LParamHi)>(BMP.Height/Message.LParamLo) then
Begin
CoeffD := BMP.Width;
CoeffM := Message.LParamHi;
End Else
Begin
CoeffD := BMP.Height;
CoeffM := Message.LParamLo;
End;
ICO.Width := MulDiv(BMP.Width,CoeffM,CoeffD);
ICO.Height := MulDiv(BMP.Height,CoeffM,CoeffD);
ICO.Canvas.StretchDraw(ICO.Canvas.ClipRect,BMP);
DwmSetIconicThumbnail(Handle,ICO.Handle,DWM_SIT_DISPLAYFRAME);
Finally
ICO.Free;
End;
End;

II-C-3 - Donner un aperçu de la fiche

Quand on survole une des icônes de l'application dans la barre des tâches, Windows affiche une prévisualisation de la fiche. Windows demande cet aperçu via le message WM_DWMSENDICONICLIVEPREVIEWBITMAP et on donne l'image via la fonction DwmSetIconicLivePreviewBitmap.

DwmSetIconicLivePreviewBitmap doit recevoir une image de type DIB 32 bits et une position d'affichage. Cette position est relative par rapport au coin supérieur gauche de la fiche principale de l'application. Le code de création de l'aperçu est le même que celui du paragraphe précédent. Le calcul de la position est effectué à partir de la position de la zone MDICliente de la fiche principale.

On part de la position de la zone MDIClient donnée par les coordonnées du handle de fenêtre MainForm.ClientHandle. (flèche bleue)
On soustrait la position absolue de la fiche principale. (flèche rouge)
On ajoute la position relative de la fiche fille. (flèche jaune).
La position obtenue (flèche verte) est celle qu'il faut transmettre à la fonction DwmSetIconicLivePreviewBitmap afin que l'aperçu se superpose exactement à la fiche.

if Message.Msg = WM_DWMSENDICONICLIVEPREVIEWBITMAP then
Begin
// On cherche la position de la fiche enfant par rapport au coin
// supérieur gauche de la fiche MDI principale
// On part de la position absolue de la zone MDICLIENT de la fenêtre principale
// On ajoute la position relative de la fiche enfant dans cette zone client
// On soustrait la position absolue de la fiche principale
GetWindowRect(MainForm.ClientHandle,Rect);
Point.X := Rect.Left + MDIFille.Left - MainForm.Left;
Point.Y := Rect.Top  + MDIFille.Top  - MainForm.Top;
// Dans le cas d'un aperçu taille réelle on donne directement le bitmap
DwmSetIconicLivePreviewBitmap(Handle,bmp.Handle,Point,DWM_SIT_DISPLAYFRAME);
End Else

II-C-4 - Gérer le titre de la fenêtre

Windows se sert du titre de la fenêtre comme titre des icônes. Comme Windows ne connait que la fenêtre 'cache', on doit recopier le titre de la fenêtre fille à la fenêtre cache à chaque changement de celui-ci.

Pour cela nous allons gérer le message CM_TEXTCHANGED de la fiche MDIChild. À chaque changement de titre, le titre de la fenêtre « cache » sera mis à jour.

TMDIChild = class(TForm)
{...}
private
{ Déclarations privées }
procedure CMTextChanged (var Message: TMessage); message CM_TEXTCHANGED;
end;

{...}

procedure TMDIChild.CMTextChanged(var Message: TMessage);
begin
if Cache<>Nil then
Cache.Caption := Caption;
Inherited;
end;

II-C-5 - Changer l'icône représentant la fiche

Windows va conserver l'icône de la fiche tant que la fiche existe. Il ne la redemandera pas au prochain affichage de la barre de boutons dans la barre des tâches.
Il est possible de forcer Windows à demander de nouveau l'icône au prochain affichage. Il faut appeler DwmInvalidateIconicBitmaps afin d'invalider l'icône en mémoire.

Dans notre exemple nous le ferons chaque fois que la taille de la fille MDIChild change. Ainsi, au moins, les proportions de l'icône seront exactes à chaque affichage. Dans l'évènement OnResize de la fiche MDIChild, l'icône en cache sera invalidée.

TFormCache = class(TForm)
{...}
public
{ Déclarations publiques }
Procedure DoReSize;
end;

procedure TFormCache.DoReSize;
begin
// Quand la fiche fille a changé de taille, on demande à windows
// d'effacer son cache afin que la nouvelle icône ait les bonnes
// proportions.
DwmInvalidateIconicBitmaps(Handle);
end;
{...}

procedure TMDIChild.FormResize(Sender: TObject);
begin
if Cache<>Nil then
Cache.DoReSize;
end;

II-D - Gérer les actions sur les icônes

Du fait d'utiliser une fiche de cache au lieu des fiches elles-mêmes, les fiches MDIChild ne reçoivent plus les messages standards comme ceux pour les activer ou les fermer. Ces messages sont reçus par la fenêtre « cache ». Il faut donc intercepter ces messages et les transmettre la fiche MDIChild correspondante.

Nous allons donc compléter le code de WndProc pour gérer au minimum les messages WM_CLOSE et WM_ACTIVATE. WM_CLOSE est envoyé si l'utilisateur clique sur la petite croix rouge située sur l'icône. WM_ACTIVATE est envoyé si l'utilisateur clique sur l'une des icônes.

procedure TFormCache.WndProc(var Message: TMessage);
begin
if Message.Msg = WM_CLOSE Then
begin
// La fiche a reçu une demande de fermeture venant de la barre de tâche
// On la transmet à la vraie fenêtre fille
PostMessage(MDIFille.Handle,WM_CLOSE,0,0);
end else
if Message.Msg = WM_ACTIVATE Then
begin
// La fiche a reçu une demande d'activation venant de la barre de tâche
// (Quand on clique sur l'une des icônes dans Win 7 Aero)
// On met la vraie fiche fille en avant-plan
MDIFille.Show;
end else
{...}
End;

III - Conclusion

Nous aurons vu comment gérer la barre des tâches dans une application de type MDI. Il reste des points à améliorer comme la qualité des icônes produites étant donné que la méthode utilisée (ici avec un simple StretchDraw) n'est pas optimale.
Il est possible aussi de gérer l'ordre des documents dans la barre des tâches, ici nous avons positionné chaque nouveau bouton en fin, mais il est possible de le positionner où on veut par rapport aux boutons existants.

Les fichiers du projet sont disponibles ici : TestMDI2010.zip ( Miroir )
Remarque : il faut Delphi 2010 minimum pour pouvoir ouvrir ce projet.

Je tiens à remercier Julien Lelong et Franck Soriano pour la relecture et les corrections.