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