UITypeEditor : les éditeurs de types graphiques

Les éditeurs de types

Introduction

Les éditeurs de types permettent de dépasser les simples convertisseurs de type. Un convertisseur de type permet d'éditer un type sous forme de chaîne, mais on arrive très vite aux limites de ce système. Par exemple, on pourrait très bien demander à l'utilisateur de saisir le nom ou le code HTML d'une couleur, mais cela est très fastidieux et oblige l'utilisateur à connaître le nom des couleurs ou savoir à quelle couleur correspond un code couleur HTML. Vous m'avouerez que cela n'est pas aisé. Enfin, comment penseriez-vous éditer une collection d'un type complexe sous forme de chaîne ? Voilà pourquoi, les éditeurs de types existent. Ils permettent d'éditer le type sous une forme graphique.

Les formes de base des éditeurs de types

Les éditeurs permettent d'éditer de façon graphique un type. Il y a trois types d'éditeurs de types :

Les éditeurs en drop down : sous forme de panel déroulant sous la case de la propriété dans la fenêtre des propriétés. Cela se prête bien aux propriétés à valeur visuelle simple (comme Anchor ou Dock).


Les éditeurs en boîte de dialogues : sous forme de fenêtre modale qui s'affiche quand on clique sur le bouton « ... » qui se place dans la case de la propriété dans la fenêtre des propriétés. Cela se prête bien aux éditions plus complexes comme les collections.


Le troisième type n'en est pas vraiment un : on peut afficher une représentation graphique de la valeur du type dans un petit rectangle à gauche du texte de la propriété.

La classe de base des éditeurs UITypeEditor

La classe UITypeEditor est la classe de base de tous les éditeurs de types. Il possède les membres suivants :

  • EditValue : méthode appelée pour commencer l'édition du type. Cette méthode fournit trois paramètres : un contexte décrivant le composant et la propriété en cours d'édition, un provider afin de récupérer les services du mode Design et la valeur à éditer. Elle renvoie la valeur éditée.
  • GetEditStyle : indique le type d'éditeur DropDown ou Form Modal ou None (par défaut).
  • GetPaintValueSupported : indique si on peut dessiner une représentation d'une valeur du type.
  • PaintValue : dessine une représentation de la valeur du type dans le petit rectangle à gauche du texte de la propriété.
  • La propriété IsDropDownResizable permet de définir si le panel de drop down d'édition est redimensionnable.

ITypeDescriptorContext

Cette interface permet d'accéder au composant, à son conteneur et au descripteur de la propriété pour laquelle la méthode EditValue est appelée. On récupère une instance d'une classe implémentant cette interface en premier paramètre (nommé context) de la méthode EditValue que l'on doit redéfinir. Cette interface possède les membres suivants (leur description est à prendre dans le contexte du mode Design) :

  • La propriété Container renvoie l'IContainer du composant qui possède une propriété du type géré par notre éditeur dont on édite la valeur.
  • La propriété Instance renvoie le composant qui possède une propriété du type géré par notre éditeur dont on édite la valeur.
  • La propriété PropertyDescriptor renvoie le descripteur de la propriété du type géré par notre éditeur dont on édite la valeur.
  • Les méthodes OnComponentChanging et OnComponentChanged permettant d'avertir l'environnement de Design que la valeur de la propriété décrite par PropertyDescriptor a changé.

Ainsi, on pourra se servir du contexte pour savoir quelle propriété de quel composant possède la valeur passée éventuellement en paramètre.

Le service IWindowsFormsEditorService

Ce service ne sert qu'à afficher les fenêtres et DropDowns des éditeurs de type. Il comprend les trois méthodes suivantes :

  • DropDownControl : permet d'ouvrir le panel déroulant d'édition sous la case de la propriété en cours d'édition.
  • CloseDropDown : permet de fermer le panel déroulant actuellement ouvert.
  • ShowDialog : permet d'afficher une fenêtre comme modal dans l'environnement de développement.

Dériver une classe de UITypeEditor

Pour créer un éditeur de type, il faut créer une classe dérivée d'UITypeEditor

Editeur de type en Drop Down

Panel d'édition

Tout d'abord, il faut créer un UserControl ou un dérivé d'un autre contrôle qui permettra d'éditer une instance du type dans le DropDown. C'est cet UserControl qui sera hébergé et affiché dans ce panel déroulant lors du click sur la flèche de déroulement dans la case de la propriété de la fenêtre des propriétés.

Cet UserControl devra avoir, normalement :

Un constructeur prenant en paramètre l'instance du type à éditer pour pouvoir initialiser l'affichage en fonction de la valeur de la propriété éditée et une instance de l'interface IWindowsFormsEditorService lui permettant de fermer le panel déroulant sur lequel il se trouve quand l'utilisateur aura fini l'édition.
Une propriété en lecture seule permettant de récupérer la valeur éditée.

Cet UserControl sera instancié depuis la méthode EditValue.

Dans cet UserControl, on effectuera tous les traitements nécessaires à la modification de la valeur passée au constructeur.

Indiquer le style DropDown

On redéfinira la méthode GetEditStyle pour renvoyer la valeur UITypeEditorEditStyle.DropDown. Cela permet à la fenêtre des propriétés de savoir qu'elle devra afficher une flèche de déroulement dans la case de la propriété à droite du texte.

Redéfinir la méthode EditValue

Cette méthode permet de commencer l'édition d'une propriété et renvoie la valeur éditée. Elle fournit trois paramètres : un contexte (de type ITypeDescriptorContext) décrivant le composant et la propriété en cours d'édition, un provider (de type IServiceProvider) afin de récupérer les services du mode Design et la valeur à éditer (de type object).

Le principal service dont on aura besoin est le service IWindowsFormsEditorService qui permet d'ouvrir et de fermer le panel d'édition.

Démarrer l'édition et ouvrir le panel

Dans la redéfinition de EditValue, il faudra opérer de la manière suivante afin d'ouvrir le panel d'édition :

Récupérer une instance du service IWindowsFormsEditorService qui permettra d'ouvrir et de fermer le panel.
Si l'on obtient bien ce service (je rappelle qu'un service n'est jamais garanti) :
Créer une instance de notre UserControl qui permettra d'éditer la valeur avec la valeur à éditer et le service précédemment récupéré.
Appeler la méthode DropDownControl du service IWindowsFormsEditorService en lui passant l'instance de l'UserControl créée à l'étape précédente. Cela permet d'ouvrir le panel de façon modale de sorte que cette méthode ne renvoie la main qu'une fois le panel fermé.
Renvoyer la valeur éditée (depuis l'instance de l'UserControl)

Terminer l'édition et fermer le panel

Pour terminer l'édition, fermer le panel et renvoyer la valeur éditée. Depuis l'UserControl, on pourra utiliser le service IWindowsFormsEditorService afin d'appeler la méthode CloseDropDown pour fermer le panel et redonner la main à EditValue.

Exemple

L'éditeur de type :

/// <summary>
/// Un éditeur de propriétés par Drop Down
/// </summary>
public class MyDirEditor : UITypeEditor
{
    /// <summary>
    /// Lance l'édition d'une valeur
    /// </summary>
    /// <param name="context">définit la propriété et l'instance
    /// du composant à éditer</param>
    /// <param name="provider">permet de récupérer un service</param>
    /// <param name="value">la valeur à éditer</param>
    /// <returns>la valeur éditée</returns>
    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        //récupère le service d'affichage
        IWindowsFormsEditorService wfEditorSvc =
        (IWindowsFormsEditorService)
        provider.GetService(typeof(IWindowsFormsEditorService));

        if (wfEditorSvc != null)
        {
            //crée une instance du panel d'édition de la propriété
            //pour la drop down
            EditorPanel ep = new EditorPanel(
            (Direction)context.PropertyDescriptor.GetValue(
            context.Instance),wfEditorSvc);
            //ouvre la dropdown contenant notre panel
            wfEditorSvc.DropDownControl(ep);
            //récupère la valeur
            return ep.Dir;
        }
        return Shape.Rectangle;
    }
    /// <summary>
    /// Permet d'indiquer le type d'édition :
    /// DropDown pour contrôle déroulant
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override UITypeEditorEditStyle GetEditStyle(
    ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
}

Le panel d'édition (UserControl) :

/// <summary>
/// Contrôle qui sera affiché dans la dropdown de la propriété
/// </summary>
public partial class EditorPanel : UserControl
{
    //service d'affichage pour les éditeurs
    private IWindowsFormsEditorService editorSvc;
    private LesDesignersLib.EditorUserControl.Direction dir;
    
    /// <summary>
    /// une propriété pour lire la valeur éditée
    /// </summary>
    public LesDesignersLib.EditorUserControl.Direction Dir
    {
        get { return this.dir; }
    }
    public EditorPanel()
    {
        InitializeComponent();
    }
    
    /// <summary>
    /// construit le panel à partir de la valeur de la propriété à éditer
    /// et du service d'affichage des éditeurs (pour pouvoir fermer le panel)
    /// </summary>
    /// <param name="dir"></param>
    /// <param name="editorSvc"></param>
    public EditorPanel(LesDesignersLib.EditorUserControl.Direction dir, IWindowsFormsEditorService editorSvc) : this()
    {
        this.editorSvc = editorSvc;
        this.dir = dir;

        //init les cases à cocher suivant les flags dans la valeur à éditer
        foreach (string s in Enum.GetNames(typeof(LesDesignersLib.EditorUserControl.Direction)))
        {
            lstChkList.Items.Add(s);
        }
        if ((dir & EditorUserControl.Direction.Left) == EditorUserControl.Direction.Left)
            lstChkList.SetItemChecked(0, true);
        if ((dir & EditorUserControl.Direction.Right) == EditorUserControl.Direction.Right)
            lstChkList.SetItemChecked(1, true);
        if ((dir & EditorUserControl.Direction.Top) == EditorUserControl.Direction.Top)
            lstChkList.SetItemChecked(2, true);
        if ((dir & EditorUserControl.Direction.Bottom) == EditorUserControl.Direction.Bottom)
            lstChkList.SetItemChecked(3, true);
    }

    private void lstChkList_ItemCheck(object sender, ItemCheckEventArgs e)
    {
        //met à jour la valeur de la propriété
        LesDesignersLib.EditorUserControl.Direction tmp = (LesDesignersLib.EditorUserControl.Direction) Enum.Parse(typeof(LesDesignersLib.EditorUserControl.Direction),lstChkList.Items[e.Index].ToString());
        if (e.NewValue == CheckState.Checked)
            this.dir |= tmp;
        else
            this.dir &= ~tmp;
    }

    private void btnClose_Click(object sender, EventArgs e)
    {
        //ferme ce panel éditeur
        editorSvc.CloseDropDown();
    }
}

La propriété marquée :

[Flags()]
public enum Direction
{
    Left = 1,
    Right = 2,
    Top = 4,
    Bottom = 8
}

private Direction m_Dir;

/// <summary>
/// Une propriété éditée avec un éditeur personnalisé de type DropDown
/// (Editor) permet de préciser la classe qui implémente l'Editor
/// </summary>
[EditorAttribute(typeof(LesDesignersLib.EditorUserControl.MyDirEditor),typeof(UITypeEditor))]
public Direction Dir
{
    get { return m_Dir; }
    set { m_Dir = value; }
}

Editeur de type en fenêtre Modale

Form d'édition

Tout d'abord, il faut créer une Form qui permettra d'éditer une instance du type. C'est cette Form qui sera affichée de façon modale lors du click sur le bouton « ... » de la case de la propriété de la fenêtre des propriétés.

Cette Form devra avoir, normalement (tout comme l'UserControl dans le cas de DropDown) :

Un constructeur prenant en paramètre l'instance du type à éditer pour pouvoir initialiser l'affichage en fonction de la valeur de la propriété éditée.
Un bouton OK et un bouton Cancel qui ont leur propriété DialogResult respectivement à DialogResult.OK et DialogResult.Cancel. De plus, ces boutons peuvent être les AcceptButton et CancelButton de la Form.
Une propriété en lecture seule permettant de récupérer la valeur éditée.
Cette Form sera instanciée depuis la méthode EditValue.

Indiquer le style « fenêtre Modale »

On redéfinira la méthode GetEditStyle pour renvoyer la valeur UITypeEditorEditStyle.Modal. Cela permet à la fenêtre des propriétés de savoir qu'elle devra afficher un bouton « ... » dans la case de la propriété à droite du texte.

Redéfinir la méthode EditValue

Cette méthode permet de commencer l'édition d'une propriété et renvoie la valeur éditée. Elle fournit trois paramètres : un contexte (de type ITypeDescriptorContext) décrivant le composant et la propriété en cours d'édition, un provider (de type IServiceProvider) afin de récupérer les services du mode Design et la valeur à éditer (de type object).

Le principal service dont on aura besoin est le service IWindowsFormsEditorService qui permet d'ouvrir la Form de façon modale en tenant compte de l'environnement de Design.

Ouvrir la Form Modale

Dans la redéfinition de EditValue, il faudra opérer de la manière suivante afin d'ouvrir la fenêtre modale d'édition :

Récupérer une instance du service IWindowsFormsEditorService qui permettra d'ouvrir la fenêtre modale dans l'environnement.
Si l'on obtient bien ce service (je rappelle qu'un service n'est jamais garanti) :
Créer une instance de notre Form qui permettra d'éditer la valeur avec la valeur à éditer.
Appeler la méthode ShowDialog du service IWindowsFormsEditorService en lui passant l'instance de la Form créée à l'étape précédente. Cela permet d'ouvrir la Form de façon modale dans l'environnement de sorte que cette méthode ne renvoie la main qu'une fois le panel fermé. On n'utilisera pas directement la méthode ShowDialog de la Form sinon notre fenêtre pourrait être cachée par l'environnement (dans la mesure où il n'y a pas de service pour récupérer la fenêtre qui doit être parente de notre Form). C'est le service qui appellera la méthode ShowDialog de notre Form avec les bons paramètres.
Si le résultat de ShowDialog est DialogResult.OK, alors renvoyer la valeur éditée (depuis l'instance de la Form).
Sinon lancer une exception ou renvoyer null/Nothing ou une valeur par défaut ou la valeur reçue en paramètre.

Exemple

L'éditeur de type :

/// <summary>
/// Un éditeur de propriétés par boîte de dialogue
/// </summary>
public class MyCultureEditor : UITypeEditor
{
    /// <summary>
    /// Lance l'édition d'une valeur
    /// </summary>
    /// <param name="context">définit la propriété et
    /// l'instance du composant à éditer</param>
    /// <param name="provider">permet de récupérer un service</param>
    /// <param name="value">la valeur à éditer</param>
    /// <returns>la valeur éditée</returns>
    public override object EditValue(ITypeDescriptorContext context,IServiceProvider provider, object value)
    {
        //récupère le service d'affichage
        IWindowsFormsEditorService wfEditorSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));

        if (wfEditorSvc != null)
        {
            //crée une instance de notre boîte de dialogue d'édition
            FormEditor fe = new FormEditor((CultureInfo)context.PropertyDescriptor.GetValue(context.Instance));

            //affiche la boîte de dialogue
            if (wfEditorSvc.ShowDialog(fe) == DialogResult.OK)
            {
                //renvoie la valeur éditée
                return fe.Culture;
            }
            else
                MessageBox.Show("Opération annulée");
        }
        return Shape.Rectangle;
    }
    /// <summary>
    /// Permet d'indiquer le type d'édition : Modal pour boîte de dialogue
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.Modal;
    }
}

La fenêtre d'édition :

/// <summary>
/// Form utilisée comme éditeur de propriétés
/// </summary>
public partial class FormEditor : Form
{
    public FormEditor()
    {
        InitializeComponent();
    }

    /// <summary>
    /// Construit une fenêtre d'édition de la valeur
    /// </summary>
    /// <param name="culture"></param>
    public FormEditor(CultureInfo culture) : this()
    {
        switch (culture.TwoLetterISOLanguageName)
        {
        case "DE":
            radDE.Checked = true;
            break;
        case "FR":
            radFR.Checked = true;
            break;
        case "EN":
            radEN.Checked = true;
            break;
        default:
            radFR.Checked = true;
            break;
        }
    }

    /// <summary>
    /// récupère la valeur modifiée
    /// </summary>
    public CultureInfo Culture
    {
        get {
            if (radFR.Checked)
                return new CultureInfo("FR");
            else if (radDE.Checked)
                return new CultureInfo("DE");
            else
                return new CultureInfo("EN");
        }
    }
}

La propriété :

private CultureInfo myCulture = System.Threading.Thread.CurrentThread.CurrentUICulture;

/// <summary>
/// Une propriété éditée avec un éditeur personnalisé de type Dialogue
/// (Editor) permet de préciser la classe qui implémente l'Editor
/// (EditorBrowsable) indique que la propriété est toujours éditable
/// avec un éditeur
/// </summary>
[EditorAttribute(typeof(LesDesignersLib.EditorUserControl.MyCultureEditor), typeof(UITypeEditor))]
[EditorBrowsable(EditorBrowsableState.Always)]
public Object MyCulture
{
    get { return myCulture; }
    set { myCulture = (CultureInfo)value; }
}

Dessiner une représentation d'une valeur du type

Introduction

Cette technique ne fait pas partie des types d'éditeurs car on peut très bien l'associer à une édition DropDown ou Modal ou encore, simplement dessiner la représentation sans édition particulière (par exemple, une couleur).

Indiquer que l'on veut dessiner la représentation d'une valeur

Tout d'abord, il faut redéfinir la méthode GetPaintValueSupported afin de spécifier que l'on veut dessiner une valeur en renvoyant true. Cela permet à la fenêtre des propriétés de dessiner le petit rectangle à gauche de la propriété.

Dessiner la représentation de la valeur demandée

Ensuite, il faut redéfinir la méthode PaintValue. Cette méthode permet de dessiner la valeur dans le petit rectangle prévu à cet effet. Elle prend en paramètre une instance du type PaintValueEventArgs qui comprend les membres suivants :

Bounds : indique les coordonnées du rectangle dans lequel dessiner (Rectangle).
Context : indique la propriété et le composant en cours d'édition (ITypeDescriptorContext).
Graphics : permet de dessiner dans le rectangle prévu à cet effet (Graphics).
Value : la valeur dont on doit dessiner la représentation visuelle (object).

On pourra donc dessiner Value sur le Graphics en ne dépassant pas les Bounds.

Exemple

L'éditeur de type :

/// <summary>
/// Un éditeur de propriétés qui dessine la valeur de la propriété
/// dans le rectangle à côté du texte de la valeur
/// </summary>
public class MyShapeEditor : UITypeEditor
{
    /// <summary>
    /// Permet d'indiquer si l'éditeur peut dessiner
    /// la valeur de la propriété à côté du texte de la valeur
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override bool GetPaintValueSupported(ITypeDescriptorContext context)
    {
        return true;
    }
    /// <summary>
    /// Dessine la valeur passée dans e.Value dans le petit cadre
    /// qui se trouve avant le texte de la valeur
    /// </summary>
    /// <param name="e"></param>
    public override void PaintValue(PaintValueEventArgs e)
    {
        //récupère la valeur
        Shape sh = (Shape)e.Value;
        //crée un pinceau pour dessiner
        using(Pen p = new Pen(Color.Purple,2))
        {
            //dessine la forme en fonction de la valeur
            switch (sh)
            {
            case Shape.Circle:
                //dessine un cercle au milieu
                Rectangle r = new Rectangle(
                e.Bounds.Left +
                (e.Bounds.Width-e.Bounds.Height) / 2,
                e.Bounds.Top+1,
                e.Bounds.Height-2,
                e.Bounds.Height-2);
                e.Graphics.DrawEllipse(p, r);
                break;
            case Shape.Rectangle:
                //dessine le rectangle
                e.Graphics.DrawRectangle(p, e.Bounds);
                break;
            case Shape.Ellipse:
                //dessine l'ellipse
                e.Graphics.DrawEllipse(p, e.Bounds);
                break;
            default:
                break;
            }
        }
    }
}

La propriété :

public enum Shape
{
    Circle,
    Rectangle,
    Ellipse
}

private Shape myShape;

/// <summary>
/// Une propriété éditée avec un éditeur personnalisé
/// qui dessine la figure à côté de la valeur de la propriété
/// (Editor) permet de préciser la classe qui implémente l'Editor
/// </summary>
[EditorAttribute(typeof(LesDesignersLib.EditorUserControl.MyShapeEditor),typeof(UITypeEditor))]
public Shape MyShape
{
    get { return myShape; }
    set { myShape = value; }
}

Appliquer l'éditeur de type

[EditorAttribute(typeof(LesDesignersLib.EditorUserControl.MyShapeEditor), typeof(UITypeEditor))]
public Shape MyShape
{
    // ...

Références

How to: Implement a UI Type Editor

Walkthrough: Implementing a UI Type Editor

A voir également
Ce document intitulé « UITypeEditor : les éditeurs de types graphiques » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous