Developpez.com - Microsoft DotNET
X

Choisissez d'abord la catégorieensuite la rubrique :



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

Version Visual Basic

Par CGi et Neo.51

Le 17 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 :

Imports System
Imports System.Windows.Forms

Namespace Composant
   Public Class Horloge
      Inherits Control

      Public Sub New()
      End Sub
   End Class
End Namespace

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.vb".


Code du contrôle :

Fichier Composant.vb :

Imports System
Imports System.Drawing
Imports System.Windows.Forms
Imports System.ComponentModel

Namespace Composant

   Public Class Horloge
      Inherits Control
      Private t As New Timer()
      Private fAiguilleSec As Boolean = True

      '--------------------------------------------------------
      Public Sub New() 'constructeur
         SetStyle(ControlStyles.DoubleBuffer Or ControlStyles.UserPaint Or _
                                      ControlStyles.AllPaintingInWmPaint, True)
         AddHandler t.Tick, AddressOf Tempo
         t.Interval = 1000
         t.Start()
      End Sub 'New
      '--------------------------------------------------------

      Protected Overrides ReadOnly Property DefaultSize() As Size
         Get
            Return New Size(121, 121)
         End Get
      End Property

      '--------------------------------------------------------
      Private Sub Tempo(myObject As [Object], myEventArgs As EventArgs)
         Invalidate()
      End Sub

      '--------------------------------------------------------
      <Category("Appearance"), Description("Affiche l'aiguille des secondes."), _
      DefaultValue(True)> _
      Public Property AiguilleSec() As Boolean
         Get
            Return fAiguilleSec
         End Get
         Set
            fAiguilleSec = value
            Invalidate()
         End Set
      End Property

      '--------------------------------------------------------
      Protected Overrides Sub OnPaint(e As PaintEventArgs)
         Dim blackPen As New Pen(Color.Black, 1)
         Dim rect As New Rectangle(2, 2, 118, 118)
         e.Graphics.DrawEllipse(blackPen, rect)

         Dim d As DateTime = DateTime.Now
         Dim n, z, u, x0, y0, x1, y1, x2, y2, x3, y3 As Single

         Dim aigPoints(3) As PointF

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

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

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

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

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

            Dim redBrush As 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()
         End If
         '-----------------------
         ' Aiguille des Minutes
         n = d.Minute * 200 / 60
         z = n / 100 * 3.14159F
         u =(n + 50) / 100 * 3.14159F

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

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

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

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

         Dim limeBrush As 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 = CSng(Math.Sin(z)) * 35
         y0 = CSng(- Math.Cos(z)) * 35

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

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

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

         Dim yellowBrush As 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
         Dim drawFont As New Font("Arial", 8)
         Dim drawBrush As 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()
         MyBase.OnPaint(e)
      End Sub 'OnPaint

      '--------------------------------------------------------
      Protected Overloads Overrides Sub Dispose(disposing As Boolean)
         If disposing And Not (t Is Nothing) Then
            t.Dispose()
            t = Nothing
         End If
         MyBase.Dispose(disposing)
      End Sub 'Dispose
   End Class 'Horloge
End Namespace 'Composant

En plus des directives "Imports" 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 Or ControlStyles.UserPaint Or _
                                    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 Then
   '....

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 Property AiguilleSec() As Boolean
         Get
            Return fAiguilleSec
         End Get
         Set
            fAiguilleSec = value
            Invalidate()
         End Set
      End Property

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 le signe "<" et le signe ">" :

      <Category("Appearance"), Description("Affiche l'aiguille des secondes."), _
      DefaultValue(True)> _
      Public Property AiguilleSec() As Boolean
      '....

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'outil IDE, vous pouvez toujours le compiler en lignes de commandes :

    vbc /target:library /r:System.dll /r:System.Drawing.dll
                                  /r:System.Windows.Forms.dll composant.vb

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.vb :
Imports System
Imports System.Drawing
Imports System.Windows.Forms

Namespace MonProjet
   Public Class WinForm
      Inherits Form
      Private horloge1 As New Composant.Horloge()

      Public Sub New()
         MyBase.New()
         horloge1.Location = New Point(20, 20)

         Size = New Size(170, 200)
         Text = "WinForm"

         Controls.Add(Me.horloge1)
      End Sub 'New

      Public Shared Sub Main()
         Application.Run(New WinForm())
      End Sub 'Main

   End Class 'WinForm
End Namespace 'MonProjet

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 :
    vbc /target:winexe /r:System.dll /r:System.Drawing.dll
                    /r:System.Windows.Forms.dll /r:composant.dll WinForm.vb

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



Bonne composition.
CGi et Neo.51


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-2016 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.

Responsable bénévole de la rubrique Microsoft DotNET : Hinault Romaric -