Utilisation des API Windows dans les composants Winform

15 février 2006 12:00 par Franck Desbrosses

Les composants fournis avec le framework 1.1 font tout ce qu’ils peuvent, et c’est souvent suffisant, mais dans certains cas, on aimerait bien obtenir le même comportement que ceux fournis avec Windows XP.

Hors pour cela, hormis redévelopper tous les composants, il n’y a qu’un seul moyen : passer par les API windows.

Pour illustrer cela, nous allons répondre à un besoin souvent exprimé dans les applications clientes : afficher le sens du tri sur une colonne d’un ListView en dessinant une image à côté du libellé de la colonne.

L’astuce consiste en fait à envoyer un message à l’entête des colonnes du ListView pour lui « dire » de dessiner une image.

Pour cela, nous allons créer un nouveau composant qui va hériter du contrôle ListView.

Ensuite, il faut déclarer les méthodes et les structures nécessaires pour l’utilisation des API :

   1: /// <summary>
   2: /// Structure définissant un item dans un entête
   3: /// <summary>
   4: [StructLayout(LayoutKind.Sequential)]
   5: public struct HDITEM
   6: {
   7:     public Int32 mask;
   8:     public Int32 cxy;
   9:     [MarshalAs(UnmanagedType.LPTStr)]
  10:     public String pszText;
  11:     public IntPtr hbm;
  12:     public Int32 cchTextMax;
  13:     public Int32 fmt;
  14:     public Int32 lParam;
  15:     public Int32 iImage;
  16:     public Int32 iOrder;
  17: };
  18:  
  19: /// <summary> 
  20: /// Permet d'envoyer un message standard au contrôle
  21: /// </summary>
  22: [DllImport("USER32.DLL", EntryPoint = "SendMessage")]
  23: private static extern IntPtr SendMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam);
  24:  
  25: /// <summary>
  26: /// Permet d'envoyer un message au contrôle en lui spécifiant une référence à un item d’entête
  27: /// </summary>
  28: [DllImport("user32", EntryPoint = "SendMessage")]
  29: private static extern IntPtr SendMessageItem(IntPtr Handle, Int32 msg, IntPtr wParam, ref HDITEM lParam);
  30:  
  31: /// <summary>
  32: /// Gère le formatage des entêtes
  33: /// </summary>
  34: private enum HeaderFormatValue : int
  35: {
  36:     HDF_LEFT = 0x0000,
  37:     HDF_RIGHT = 0x0001,
  38:     HDF_CENTER = 0x0002,
  39:     HDF_JUSTIFYMASK = 0x0003,
  40:     HDF_RTLREADING = 0x0004,
  41:     HDF_OWNERDRAW = 0x8000,
  42:     HDF_STRING = 0x4000,
  43:     HDF_BITMAP = 0x2000,
  44:     HDF_BITMAP_ON_RIGHT = 0x1000,
  45:     HDF_IMAGE = 0x0800
  46: };
  47:  
  48: /// <summary>
  49: /// Gère les items dans les entêtes
  50: /// </summary>
  51: private enum HeaderItemValue : int
  52: {
  53:     HDI_WIDTH = 0x0001,
  54:     HDI_HEIGHT = HDI_WIDTH,
  55:     HDI_TEXT = 0x0002,
  56:     HDI_FORMAT = 0x0004,
  57:     HDI_LPARAM = 0x0008,
  58:     HDI_BITMAP = 0x0010,
  59:     HDI_IMAGE = 0x0020,
  60:     HDI_DI_SETITEM = 0x0040,
  61:     HDI_ORDER = 0x0080,
  62:     HDI_FILTER = 0x0100
  63: };
  64:  
  65: /// <summary>
  66: /// Gère les méthodes dans les entêtes
  67: /// </summary>
  68: private enum HeaderMethodValue : int
  69: {
  70:     HDM_GETHEADER = 0x1000 + 31,
  71:     HDM_SETITEM = 0x1200 + 4,
  72:     HDM_SETIMAGELIST = 0x1200 + 8,
  73:     HDM_GETIMAGELIST = 0x1200 + 9
  74: };

Dans le constructeur du contrôle, on s’abonne à l’événement qui est déclenché à la création du handle :

   1: public TekigoListView() : base()   
   2: {
   3:     // Abonnement à l'événement déclenché à la création du handle
   4:     this.HandleCreated += new EventHandler(TekigoListView_HandleCreated);
   5: }

Ceci nous permet de sauvegarder le handle de l’entête dans une variable privée :

   1: private void TekigoListView_HandleCreated(object sender, EventArgs e)   
   2: {  
   3:     […]
   4:  
   5:     // On envoie un message au contrôle pour lui demander de nous fournir le handle de l’entête
   6:     hdlHeader = SendMessage(this.Handle, (int)HeaderMethodValue.HDM_GETHEADER, IntPtr.Zero, IntPtr.Zero);   
   7:  
   8:     // Si le handle n'a pas été trouvé, on lève une exception
   9:     if (hdlHeader == IntPtr.Zero)
  10:           throw new SystemException("Impossible de récupérer le handle de l'entête de colonnes");
  11:  
  12:     […]
  13: }

Nous allons voir maintenant la méthode qui nous permet d’envoyer un message ordonnant au contrôle de dessiner notre image :

   1: private void DisplaySortOrder(int columnIndex, SortOrder order)
   2: {
   3:     // Création de la structure contenant l'item de l'entête
   4:     HDITEM hdItem = new HDITEM();
   5:  
   6:     // On teste pour savoir si on doit afficher ou effacer une image
   7:     if (order == SortOrder.None)
   8:     {
   9:         // Valorisation des propriétés de la structure pour n'afficher uniquement que le texte de l'entête de la colonne
  10:         hdItem.mask = (int)HeaderItemValue.HDI_FORMAT;
  11:         hdItem.fmt = (int)HeaderFormatValue.HDF_STRING;
  12:     }
  13:     else
  14:     {
  15:         // On récupère l’image correspondant au sens du tri :
  16:         Bitmap displayImage = GetBitmap(order);
  17:  
  18:         // Valorisation des flags permettant d'afficher une image
  19:         hdItem.mask = (int)(HeaderItemValue.HDI_BITMAP | HeaderItemValue.HDI_FORMAT);
  20:         hdItem.fmt = (int)(HeaderFormatValue.HDF_STRING | HeaderFormatValue.HDF_BITMAP | HeaderFormatValue.HDF_BITMAP_ON_RIGHT);
  21:  
  22:         /* Il est à noter qu’on pourrait passer par l’index d’une image (HDITEM.iImage) grâce une imageList associée (smallImageList ou StateImageList), mais cela pose deux problèmes :
  23:         d’une part l’imageList étant partagée, il faut donc réserver les deux premiers index pour les images du tri; d’autre part, si on ne désire pas afficher d’images dans la première colonne des lignes, un décallage blanc apparait du fait que le listView laisse de la place au cas où on désire afficher une image.
  24:         Du coup, il est préférable de passer par un pointeur sur une image, cela évite tout effet de bord !*/
  25:  
  26:         // Valorisation du handle du bitmap à afficher
  27:         hdItem.hbm = displayImage.GetHbitmap();
  28:     }
  29:  
  30:     // Envoi du message au listView, et plus particulièrement à son entête grâce au handle qu’on a récupéré à la création du contrôle
  31:     SendMessageItem(hdlHeader, (int)HeaderMethodValue.HDM_SETITEM, new IntPtr(columnIndex), ref hdItem);
  32: }

Il ne reste plus qu’à appeler notre méthode d’affichage au moment opportun; par exemple, sur l’événement déclenché lors du click sur l’entête d’une colonne.

L'apport des API dans les composants permet d'aller très loin dans la personnalisation des composants mais il faut tout de même rester prudent lorsqu'on a recourt à ce genre de manipulation car nous ne sommes plus en code managé, et toute erreur peut devenir fatale !