Introduction
Cet article explique comment concevoir un contrôle de barre de progression (ProgressBar) entièrement personnalisé en utilisant GDI+ dans un environnement .NET WinForms. L'objectif est d'aller au-delà des simples barres horizontales et de proposer des rendus visuels variés : circulaire (Pie), arc (Arc) et bien d'autres.
Les techniques employées reposent sur les bases déjà vues dans les articles précédents (double buffer, utilisation de System.Windows.Forms.Control, etc.). La nouveauté réside dans l'imagination et la flexibilité offerte par l'implémentation.
Analyse préliminaire
Pourquoi un contrôle personnalisé ?
Le contrôle ProgressBar natif offre un style limité et ne permet pas une personnalisation poussée (formes, couleurs, animations). Un contrôle maison permet d'obtenir des visuels modernes et adaptés à chaque besoin.
Objectifs fonctionnels
- Trois formes principales : Barre horizontale (Bar), Disque (Pie), Arc (Arc).
- Affichage du pourcentage : possibilité d'afficher ou non la valeur en texte.
- Multiples styles de progression :
- Continu (Continuous)
- Blocs (Blocks)
- Mixte (Blocs puis continu)
- Hélice (Helical) – uniquement pour la forme Barre.
- Mode indéterminé (Marquee) :
- Balancement (Swing)
- Traversée (Cross)
- Va-et-vient continu (Reciprocation Continuous)
- Glissement continu (Across Continuous)
- Va-et-vient par blocs (Reciprocation Blocks)
- Glissement par blocs (Across Blocks)
- Hélice (Helical)
- Personnalisation des dimensions et couleurs :
- Épaisseur et couleur du cadre
- Taille et couleur de l'arrière-plan
- Position et couleur du tracé de progression
- Couleur du texte de progression
- Épaisseur de la ligne (Arc) – seulement pour la forme Arc
- Taille des blocs (uniquement pour le style Blocs)
Décomposition du contrôle
Éléments constitutifs
La barre de progression est composée de trois parties :
- Progression : la partie qui indique l'avancement.
- Cadre : le contour (optionnel).
- Arrière-plan : la zone de fond.
Ces trois éléments sont visibles dans les captures suivantes (illustrations des trois formes).


### Propriétés des éléments
Cadre
La seule propriété est l'épaisseur du cadre (_SurroundLineSize). Si elle vaut 0, le cadre est invisible. Si elle dépasse la moitié de la hauteur du contrôle, tout le contrôle devient la couleur du cadre (car l'arrière-plan est dessiné en premier, puis le cadre par‑dessus).

#### Arrière-plan
La propriété _ShrinkSize (largeur de rétrécissement) définit la taille de l'arrière-plan. Si elle vaut 0, l'arrière-plan occupe tout le contrôle. Si elle vaut x, la largeur de l'arrière-plan = Largeur du contrôle - 2*x, la hauteur = Hauteur - 2*x. Si x dépasse la moitié de la hauteur, l'arrière-plan disparaît.

#### Progression
Deux sous‑propriétés :
- Zone de dessin (
_DrawMargin) : marge entre la progression et le bord du contrôle. Si vaut 0, la progression peut occuper toute la surface. Si vaut x, la progression commence à (x,x) et se termine à (largeur-x, hauteur-x). - Style de progression (Continuous, Blocks, Helical, etc.).


##### Style spécial : Hélice (Helical)
Le style Hélice est uniquement disponible pour la forme Barre. Il consiste à dessiner une série de parallélogrammes inclinés formant une hélice. Le principe est le suivant : une image en mémoire est créée avec une largeur égale à la largeur du contrôle + la largeur d'un bloc. Les parallélogrammes sont dessinés de droite à gauche. Ensuite, on découpe l'image pour qu'elle corresponde exactement à la zone de progression. En mode Marquee (indéterminé), l'image se déplace horizontalement en boucle.
### Autres propriétés
- Épaisseur d'arc (
_ArcSize) : uniquement pour la forme Arc. La valeur par défaut est Largeur/10. - Taille des blocs (
_BlockSize) : utilisée pour les styles Blocks. La partie colorée représente 2/3 de cette taille, le tiers restant sert d'espacement.
Implémentation
Préparation
Créez une classe LProgressBar.cs qui hérite de Control (nécessite la référence System.Windows.Forms.dll). Activez le double buffering dans le constructeur et définissez les styles de contrôle nécessaires à la transparence.
Propriétés principales
_Value: entier entre 0 et 100 représentant le pourcentage._SurroundLineSize: épaisseur du cadre._SurroundLineColor: couleur du cadre._ShrinkSize: rétrécissement de l'arrière-plan._ShrinkColor: couleur de l'arrière-plan._DrawMargin: marge de dessin de la progression._ArcSize: épaisseur de la ligne (Arc)._SliderColor: couleur de la progression._BlockSize: taille des blocs._ProgressStyle: énumération définissant le style (Continuous, Blocks, Helical, etc.)._ProgressType: énumération (Bar, Pie, Arc)._ProgressMode: Known ou Marquee._MarqueeTime: durée d'un cycle Marquee._MarqueeStyle: énumération des animations Marquee._MarqueeType: Even (vitesse constante) ou SlowInSlowOut (accéléré/décéléré)._ProgressText: énumération (None, Number, Percentage)._TextColor: couleur du texte.
Rendu graphique (OnPaint)
La méthode OnPaint est surchargée. Le rendu se fait en plusieurs étapes :
- Activation du mode anti‑crénelage (
SmoothingMode.HighQuality). - Calcul des dimensions réelles de dessin en fonction de
_DrawMarginet des autres propriétés. - Dessin de l'arrière‑plan (rectangle pour Bar, ellipse pour Pie/Arc).
- Dessin du cadre (rectangle ou ellipse).
- Dessin de la progression selon le style choisi : continu (fill rectangle/arc/pie), blocs, mixte, hélice, etc.
- Affichage optionnel du texte au centre.
Pour le mode Marquee, un timer (Timer_Tick) met à jour une variable dMarqueeValue qui détermine la position ou l'étendue de l'animation. La logique de mouvement (balancier, traversée, etc.) est gérée dans l'événement.
Exemple de code simplfiié (méthode OnPaint)
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
float realWidth = Width - 2 * _DrawMargin;
float realHeight = Height - 2 * _DrawMargin;
float leftTop = _DrawMargin - 0.5f;
if (leftTop < 0) leftTop = 0;
// Arrière-plan
if (_ShrinkSize * 2 < Math.Min(Width, Height))
{
RectangleF bgRect = new RectangleF(_ShrinkSize, _ShrinkSize,
Width - 1 - 2 * _ShrinkSize,
Height - 1 - 2 * _ShrinkSize);
if (_ProgressType == ProgressType.Bar)
e.Graphics.FillRectangle(new SolidBrush(_ShrinkColor), bgRect);
else
e.Graphics.FillEllipse(new SolidBrush(_ShrinkColor), bgRect);
}
// Cadre
if (_SurroundLineSize > 0)
{
using (Pen pen = new Pen(_SurroundLineColor, _SurroundLineSize))
{
float hs = _SurroundLineSize / 2f;
RectangleF frameRect = new RectangleF(hs, hs,
Width - 1 - _SurroundLineSize,
Height - 1 - _SurroundLineSize);
if (_ProgressType == ProgressType.Bar)
e.Graphics.DrawRectangle(pen, frameRect.X, frameRect.Y, frameRect.Width, frameRect.Height);
else
e.Graphics.DrawEllipse(pen, frameRect);
}
}
// Progression
SolidBrush brushProgress = new SolidBrush(_SliderColor);
if (_ProgressMode == ProgressMode.Known)
{
float progressWidth = _Value * realWidth / 100f;
if (_ProgressStyle == ProgressStyle.Continuous)
{
if (_ProgressType == ProgressType.Bar)
e.Graphics.FillRectangle(brushProgress, leftTop, leftTop, progressWidth, realHeight);
else if (_ProgressType == ProgressType.Pie)
e.Graphics.FillPie(brushProgress, leftTop, leftTop, realWidth, realHeight, 270, _Value * 3.6f);
else // Arc
{
float arcSize = (_ArcSize > 0) ? _ArcSize : Width / 10f;
RectangleF arcRect = new RectangleF(arcSize / 2 + leftTop, arcSize / 2 + leftTop,
realWidth - arcSize, realHeight - arcSize);
using (Pen penArc = new Pen(_SliderColor, arcSize))
e.Graphics.DrawArc(penArc, arcRect, 270, _Value * 3.6f);
}
}
// Autres styles (Blocks, Helical, etc.) sont implémentés de manière similaire
}
else // Marquee
{
// Logique d'animation gérée via timer
// Utilisation de dMarqueeValue pour positionner/étendre la progression
}
// Texte
if (_ProgressText != ProgressText.None && _ProgressStyle != ProgressStyle.Marquee)
{
string text = (_ProgressText == ProgressText.Number) ? _Value.ToString() : _Value + "%";
SizeF sz = e.Graphics.MeasureString(text, Font);
e.Graphics.DrawString(text, Font, brushProgress,
(Width - sz.Width) / 2, (Height - sz.Height) / 2);
}
brushProgress.Dispose();
}
Timer pour animation Marquee
private void Timer_Tick(object sender, EventArgs e)
{
float realW = Width - 2 * _DrawMargin;
float realH = Height - 2 * _DrawMargin;
float sliderLen = (_ProgressType == ProgressType.Bar) ? realW / 5f : 72f;
float range = (_MarqueeStyle == MarqueeStyle.Swing) ? realW - sliderLen :
(_MarqueeStyle == MarqueeStyle.Cross) ? realW + sliderLen : realW;
double step = _MarqueeTime / 10.0;
double increment = 180.0 / step;
if (_MarqueeType == MarqueeType.Even)
increment = (range / step);
double period = (_MarqueeStyle == MarqueeStyle.Swing
|| _MarqueeStyle == MarqueeStyle.Reciprocation_Blocks
|| _MarqueeStyle == MarqueeStyle.Reciprocation_Continuous) ? 360.0 : 180.0;
// Mise à jour de l'état d'animation
// ... (gestion de bToRight, bAcrossed, etc.)
dMarqueeValue += increment;
Invalidate();
}
Démonstration
Un programme de démonstration permet de visualiser toutes les combinaisons de formes, styles et animations. Les captures d'écran et animations de cet article sont issues de cette application.
Remarques finales
Les animations Marquee ressemblent beaucoup aux "loaders" courants. La technique employée ici peut être réutilisée pour créer un contrôle "Loading" dédié.
N'hésitez pas à adapter et améliorer ce contrôle en fonction de vos besoins. La seule limite est votre imagination.