IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)


Création d'un contrôle WinForm pour .Net

Version C#

Par CGi

Le 11 octobre 2003




Introduction :

Dans ce tutoriel, nous allons voir comment créer un composant visuel pour WinForms.
Dans l'environnement .Net un composant visuel pour WinForm s'appelle un "Contrôle" et est donc par conséquent un descendant direct ou indirect de la classe Control de l'espace de nom System.Windows.Forms.
Pour nous aider à comprendre le principe nous nous aiderons d'un exemple, une horloge analogique qui aura la particularité d'afficher ou non l'aiguille des secondes. (Voir image ci-dessous)



Ce composant sera donc utilisable dans vos projets tout simplement en l'incorporant sur votre Winform, tout comme un contrôle standard du .Net Framework.
Et si vous avez la chance de posséder un environnement RAD pour .Net, tel Visual Studio .Net, C# Builder... vous pourrez lui associer une image pour l'intégrer à la boîte à outils.


Création du contrôle :

Pour créer notre contrôle il faut donc, comme dit précédemment, créer une classe qui héritera de la classe System.Windows.Forms.Control. Cette classe sera contenue dans une bibliothèque et sera donc compilée comme telle.
En voici le code minimum :

using System;
using System.Windows.Forms;

namespace Composant
{
  public class Horloge : Control
  {
     public Horloge() : base()
     {
     }
  }
}

Le nom de la classe est important, est doit donc être explicite car il sera le nom de type du contrôle. Dans l'exemple nous avons choisi "Horloge". Il est aussi préférable que l'espace de nom soit le même que celui de l'assembly résultant de la compilation.
Nous allons maintenant compléter le fichier source "Composant.cs".


Code du contrôle :

Fichier Composant.cs :

using System;
using System.Drawing;
using System.Windows.Forms;
using System.ComponentModel;

namespace Composant
{
  public class Horloge : Control
  {
    private Timer t = new Timer();
    private bool fAiguilleSec = true;
    //--------------------------------------------------------
    public Horloge() : base()   //constructeur
    {
      SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |		
			          ControlStyles.AllPaintingInWmPaint, true);
      t.Tick += new EventHandler(Tempo);
      t.Interval = 1000;
      t.Start();
    }
    //--------------------------------------------------------
    protected override Size DefaultSize
    {
     get
      {
        return new Size(121,121);
      }
    }
    //--------------------------------------------------------
    private void Tempo(Object myObject, EventArgs myEventArgs)
    {
      Invalidate();
    }
    //--------------------------------------------------------
    [Category("Appearance"),
    Description("Affiche l'aiguille des secondes."),
    DefaultValue(true)]
    public bool AiguilleSec
    {
       get
       {
      return fAiguilleSec;
       }
       set
       {
      fAiguilleSec = value;
      Invalidate();
       }
    }
    //--------------------------------------------------------
    protected override void OnPaint(PaintEventArgs e)
    {
      Pen blackPen = new Pen(Color.Black, 1);
      Rectangle rect = new Rectangle( 2, 2, 118, 118);
      e.Graphics.DrawEllipse(blackPen, rect);

      DateTime d  = DateTime.Now;
      float n,z,u,x0,y0,x1,y1,x2,y2,x3,y3;

      PointF[] aigPoints = new PointF[4];

      //-----------------------
      // Aiguille des Secondes (si propriété AiguilleSec à true)
      if (AiguilleSec)
      {
        n = d.Second*200/60;
        z = n/100*3.14159F;
        u = (n+50)/100*3.14159F;

        x0 = (float)Math.Sin(z)*50;
        y0 = (float)-Math.Cos(z)*50;

        x1 = (float)-Math.Sin(z)*10;
        y1 = (float)Math.Cos(z)*10;

        x2 = (float)Math.Sin(u)*2;
        y2 = (float)-Math.Cos(u)*2;

        x3 = (float)-Math.Sin(u)*2;
        y3 = (float)Math.Cos(u)*2;

        SolidBrush redBrush = new SolidBrush(Color.Red);
      
        aigPoints[0].X = x1+60;
        aigPoints[0].Y = y1+60;

        aigPoints[1].X = x2+60;
        aigPoints[1].Y = y2+60;

        aigPoints[2].X = x0+60;
        aigPoints[2].Y = y0+60;

        aigPoints[3].X = x3+60;
        aigPoints[3].Y = y3+60;

        e.Graphics.FillPolygon(redBrush, aigPoints);
        e.Graphics.DrawPolygon(blackPen, aigPoints);
        redBrush.Dispose();
      }
      //-----------------------
      // Aiguille des Minutes 

      n = d.Minute*200/60;
      z = n/100*3.14159F;
      u = (n+50)/100*3.14159F;

      x0 = (float)Math.Sin(z)*50;
      y0 = (float)-Math.Cos(z)*50;

      x1 = (float)-Math.Sin(z)*10;
      y1 = (float)Math.Cos(z)*10;

      x2 = (float)Math.Sin(u)*4;
      y2 = (float)-Math.Cos(u)*4;

      x3 = (float)-Math.Sin(u)*4;
      y3 = (float)Math.Cos(u)*4;

      SolidBrush limeBrush = new SolidBrush(Color.Lime);

      aigPoints[0].X = x1+60;
      aigPoints[0].Y = y1+60;

      aigPoints[1].X = x2+60;
      aigPoints[1].Y = y2+60;

      aigPoints[2].X = x0+60;
      aigPoints[2].Y = y0+60;

      aigPoints[3].X = x3+60;
      aigPoints[3].Y = y3+60;

      e.Graphics.FillPolygon(limeBrush, aigPoints);
      e.Graphics.DrawPolygon(blackPen, aigPoints);
      limeBrush.Dispose();
      //-----------------------
      // Aiguille des Heures

      n = d.Hour*200/12 + d.Minute*200/60/12;
      z = n/100*3.14159F;
      u = (n+50)/100*3.14159F;

      x0 = (float)Math.Sin(z)*35;
      y0 = (float)-Math.Cos(z)*35;

      x1 = (float)-Math.Sin(z)*10;
      y1 = (float)Math.Cos(z)*10;

      x2 = (float)Math.Sin(u)*4;
      y2 = (float)-Math.Cos(u)*4;

      x3 = (float)-Math.Sin(u)*4;
      y3 = (float)Math.Cos(u)*4;

      SolidBrush yellowBrush = new SolidBrush(Color.Yellow);

      aigPoints[0].X = x1+60;
      aigPoints[0].Y = y1+60;

      aigPoints[1].X = x2+60;
      aigPoints[1].Y = y2+60;

      aigPoints[2].X = x0+60;
      aigPoints[2].Y = y0+60;

      aigPoints[3].X = x3+60;
      aigPoints[3].Y = y3+60;

      e.Graphics.FillPolygon(yellowBrush, aigPoints);
      e.Graphics.DrawPolygon(blackPen, aigPoints);
      yellowBrush.Dispose();
      //-----------------------
      // Chiffres
      Font drawFont = new Font("Arial", 8);
      SolidBrush drawBrush = new SolidBrush(Color.Black);

      e.Graphics.DrawString("12", drawFont, drawBrush, 55, 6);
      e.Graphics.DrawString("6", drawFont, drawBrush, 58, 101);
      e.Graphics.DrawString("3", drawFont, drawBrush, 105, 53);
      e.Graphics.DrawString("9", drawFont, drawBrush, 8, 53);
      //-----------------------
      drawFont.Dispose();
      drawBrush.Dispose();
      blackPen.Dispose();
      base.OnPaint(e);
    }
    //--------------------------------------------------------
    protected override void Dispose(bool disposing)
    {
     if (disposing && t != null)
      {
        t.Dispose();
        t = null;
      }
     base.Dispose(disposing);
    }
  }
}

En plus des directives "using" pour les espaces de noms System et System.Windows.Forms nous aurons besoin de System.Drawing pour tout ce qui est dessin.
Nous allons maintenant détailler le code du contrôle :

Le Timer :
Comme nous aurons besoin de rafraîchir le dessin de l'horloge pour dessiner la nouvelle position des aiguilles, nous créons un Timer dont nous initialiserons la propriété Interval à 1000 ms, soit une seconde. Nous affecterons la méthode Tempo à son événement Tick et nous le démarrerons par sa méthode Start. La méthode Tempo appellera la méthode Invalidate qui provoquera le rafraîchissement du dessin en déclanchant un événement Paint.

La méthode OnPaint :
Nous allons redéfinir la méthode OnPaint de notre contrôle pour dessiner son contenu. (Cette méthode est appelée à chaque fois que le dessin du contrôle à besoin d'être redessiné.) Je ne vais pas renter dans le détail des fonctions de dessin, mais en consultant son code vous pouvez observer que l'on utilise des pinceaux (Brush) et des crayons (Pen) pour dessiner, des fontes (Font) pour écrire du texte ; tous ces objets proviennent de l'espace de nom System.Drawing.
Dans le cas de notre horloge nous dessinerons ses aiguilles à l'aide des méthodes DrawPolygon et FillPolygon son contour avec la méthode DrawEllipse ; nous écrirons les textes à l'aide de la méthode DrawString. Ces méthodes provenant de la classe System.Drawing.Graphics. Les méthodes DrawPolygon et FillPolygon utilisent un tableau de points dont les points sont calculés avec les méthodes Sin et Cos de la classe System.Math. On libère les pinceaux, crayons, fontes par l'appel de leurs méthodes Dispose, et on termine par l'appel de la méthode OnPaint de la classe de base.
Dans le constructeur du composant on peut remarquer l'appel de la méthode SetStyle :

      SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |
			          ControlStyles.AllPaintingInWmPaint, true);

Cette méthode sert à modifier les styles d'un contrôle, ici nous l'utilisons pour mettre en place un Double Buffer de dessin afin d'éviter le scintillement.

La propriété DefaultSize :
Cette propriété retourne la valeur par défaut des dimensions du contrôle par l'intermédiaire de sa méthode get. Pour donner une valeur par défaut à un contrôle, il est conseillé de redéfinir cette propriété plutôt que de définir de nouvelles dimensions dans son constructeur.

La nouvelle propriété AiguilleSec :
Dans le cas de notre horloge cette propriété sert à afficher ou ne pas afficher l'aiguille des secondes, propriété qui est donc utilisée au moment du dessin de cette même aiguille dans la méthode OnPaint :

   if(AiguilleSec)
   //....

Cette propriété sera déclarée public car elle doit être accessible en dehors de la classe. Une propriété, vue de l'extérieur se comporte comme une donnée membre (dans l'environnement .Net on dit "champ"), mais vue de l'intérieur elle cache deux méthodes get et set permettant d'accéder à ce champ qui doit donc exister dans la classe. Ces méthodes sont aussi appelées méthodes d'accesseur. Dans notre exemple le champ se nomme fAiguilleSec, il est de type booléen déclaré private et initialisé à true. La propriété doit donc être de même type que le champ lui correspondant, donc booléenne dans l'exemple.

    public bool AiguilleSec
    {
       get
       {
      return fAiguilleSec;
       }
       set
       {
      fAiguilleSec = value;
      Invalidate();
       }
    }

Comme on peut le voir dans le code la méthode get retourne la valeur du champ mémorisant la valeur de cette propriété et la méthode set permet d'affecter une nouvelle valeur à ce même champ. (Dans la méthode set d'une propriété le nom de la variable retournant la nouvelle valeur est toujours value). Dans le cas de notre horloge après l'affectation de la nouvelle valeur dans la méthode set nous appellerons la méthode Invalidate de notre contrôle afin de provoquer un rafraîchissement du dessin.
Ce système de propriétés permet de simplifier le code pendant l'utilisation des composants. La syntaxe d'utilisation d'une propriété est identique à celle d'un champ tout en préservant l'encapsulation des données. De plus il permet une intégration dans les environnements RAD car les propriétés peuvent être accessibles à la conception par l'intermédiaire des inspecteurs d'objets.

On termine par la libération du timer dans la méthode redéfinie Dispose.


Précisions pour les environnements RAD :

Juste avant la définition de la propriété AiguilleSec on peut voir ces lignes entre crochets :

    [Category("Appearance"),
    Description("Affiche l'aiguille des secondes."),
    DefaultValue(true)]

Ce sont les Attributs de la propriété, ils doivent être définis avant chaque nouvelle propriété que l'on ajoute à notre composant. Ici ils servent à fournir des informations à l'EDI pour affichage de la propriété dans sa palette d'outils.
Category sélectionne la rubrique de propriétés ou l'on veut voir apparaître la propriété. (Voir liste dans l'aide : Bibliothèque de classes -> System.ComponentModel -> CategoryAttribute) Description en donne une description qui apparaîtra dans la zone de texte en bas de la palette d'outils et DefaultValue donne sa valeur par défaut. (En leur absence la propriété sera mise dans une catégorie nommée "Divers")
Si vous définissez des attributs de propriété, il faudra rajouter la directive using pour l'espace de nom System.ComponentModel.
Pour ces environnements on peut aussi associer une icône au composant. Elle doit être contenue dans un bitmap 16 x 16 pixels et devra être ajoutée au projet avant compilation.

Voir : Procédure pour C# Builder.


Compilation en lignes de commandes :

Si vous ne possédez pas d'IDE, vous pouvez toujours le compiler en lignes de commandes :

    csc /target:library Composant.cs

Après compilation, un fichier (Assembly) nommé Composant.dll a été créé. C'est lui dont vous aurez besoin pour utiliser le contrôle. C'est que nous allons voir dans l'exemple suivant.

Exemple d'utilisation :

Avec un environnement RAD ou le composant a été intégré à la palette d'outils, il y a juste à le poser sur la WinForm comme tout autre contrôle.
L'exemple ci-dessous est destiné a être compilé en lignes de commandes.

WinForm.cs :
using System;
using System.Drawing;
using System.Windows.Forms;

namespace MonProjet
{  
  public class WinForm : Form
  {
    private Composant.Horloge horloge1 = new Composant.Horloge();

    public WinForm()
    {        
     horloge1.Location = new Point(20, 20);

     Size = new Size(170, 200);
     Text = "WinForm"; 

     Controls.Add(this.horloge1);
    }

    [STAThread]
    static void Main()
    {
      Application.Run(new WinForm());
    }
  }
}

Comme vous pouvez le constater, le positionnement de l'horloge sur une WinForm n'est pas plus compliqué que celui d'un contrôle ordinaire du .Net Framwork.

Pour faciliter la compilation nous déplacerons l'assembly Composant.dll dans le même dossier que le fichier source (Winform.cs) de l'exemple ci-dessus.


Compilation de l'exemple en lignes de commandes :
    csc /target:winexe /reference:Composant.dll WinForm.cs

Voilà, j'espère vous avoir un peu aidé à concevoir vos propres contrôles pour WinForms.



Bonne composition.
CGi


Avec la contribution d'Alacazam pour la relecture.

Télécharger les sources.





C/C++
  Les pointeurs du C/C++.   Les listes chaînées.             Liste simple.             Liste triée.             Liste double.   Les arbres.   Les tas.   Le C orienté objets ?

  1 - La fenêtre principale.   2 - Contrôles et messages.   3 - Les commandes.   4 - Dialogue std.   5 - Contexte de périph.   6 - Dessiner.   7 - Les ressources.   8 - Dialogue perso.   9 - Dialogue comm.   10 - Les accélérateurs.

Assembleur
  Assembleur sous Visual C++.

C++ BUILDER
  Trucs et astuces.   Composant.   TRichEdit.   TDrawGrid.   Application MDI.   TThread.   wxWidgets.   Style Win XP.

  Première application.   Construire un menu.   Dessiner.   Sisers, Timers...   Dialogues standards.   Dialogues perso.

DotNet
  Composant C# Builder.   Contrôle WinForm.   Application MDI.

Java
  Applet java.





Copyright 2002-2020 CGi - Tous droits réservés CGi. Toutes reproduction, utilisation ou diffusion de ce document par quelque moyen que ce soit autre que pour un usage personnel doit faire l'objet d'une autorisation écrite de la part de l'auteur, propriétaire des droits intellectuels.
Les codes sources de ce document sont fournis en l'état. L'utilisateur les utilise à ses risques et périls, sans garantie d'aucune sorte de la part de l'auteur. L'auteur n'est responsable d'aucun dommage subi par l'utilisateur pouvant résulter de l'utilisation ou de la distribution des codes sources de ce document.
De la même façon, l'auteur n'est en aucun cas responsable d'une quelconque perte de revenus ou de profits, ou de données, ou de tous dommages directs ou indirects, susceptibles de survenir du fait de l'utilisation des codes sources de ce document, quand bien même l'auteur aurait été averti de la possibilité de tels dommages. L'utilisation des codes sources de ce document vaut acceptation par l'utilisateur des termes de la licence ci-dessus.