Behavior Service : la nouvelle méthode d'ornement au Design Time

Behavior Service : la nouvelle méthode d'ornement au Design Time

Introduction

Dans la version 1.1 du framework .Net, on pouvait dessiner des « ornements » pour le Design Time en créant un ControlDesigner personnalisé et en redéfinissant la méthode OnPaintAdorments. Le problème de cette solution est que vous devez gérer vous-même le dessin de tous les ornements dans une seule méthode. De plus, si vous vouliez que les ornements soient interactifs, il fallait redéfinir des méthodes de ce même Designer comme OnMouseDragBegin et fabriquer vous-même la logique de sélection de la bonne action. Depuis la version 2.0, un nouveau service est apparu : le service de comportement au moment du Design, Behavior Service se chargeant de gérer les ornements et leur interaction avec l'utilisateur de l'environnement de développement. Cela se fait de manière indépendante et répartie : chaque ornement s'autogère.

Le service Behavior Service

Aperçu de l'architecture du Behavior Service

Plus spécifiquement le Behavior Service permet de gérer de manière uniforme toute la manipulation des éléments d'interface utilisateur comme la manipulation à la souris, avec les commandes de menus et les opérations de glisser-déposer au moment du Design.

Pour dessiner sur un contrôle au moment du Design, le service Behavior gère une série d'ornements ou adorners . Chaque ornement possède un ou plusieurs glyphs afin de réaliser le dessin d'une partie ou de la totalité de l'ornement. Un Behavior peut être associé à un glyph afin que l'utilisateur puisse interagir avec le glyph (dessin). Cependant un Behavior peut être seul et agir globalement.

Le service Behavior se compose donc d'une pile de Behaviors et d'une série d'Adorners/Glyphs.

Par défaut, on trouve quelques ornements/Behaviors :

  • L'ornement de sélection : permet de dessiner les poignées de redimensionnement et de gérer l'interaction avec la sélection
  • L'ornement de corps/body : ornement transparent recouvrant la surface du contrôle et gérant par exemple la création de l'évènement par défaut au double clic
  • L'ornement de saisie : permet de gérer le déplacement du contrôle
  • L'ornement de Smart tags (la petite flèche en haut à droite) : gère l'affichage du panel de Smart Tags.

Quelques définitions

Définition : un « glyph » est un élément graphique d'interface utilisateur, c'est-à-dire n'importe quelle zone ajoutée au contrôle et qui peut interagir avec lui au clic, déplacement de souris : par exemple, les poignées de redimensionnement. Elle assure son dessin et permet de tester si la souris se trouve dans sa surface. Un glyph n'implémente que le dessin pas l'interaction.

Définition : un « adorner » est un ensemble de « glyphes » qui peut être désactivé par le « BehaviorService ». C'est une couche invisible.

Définition : un « behavior » associe une interaction associée à un « glyph ».

Définition : le « BehaviorService » permet de gérer l'interaction de l'utilisateur avec les « glyphes » présents sur chaque contrôle. Il se charge de tester sur quel glyph se trouve la souris et d'exécuter le bon « behavior » associé au glyph.

Création d'Adorner, de Glyph et de Behavior

Assemblies

Tout d'abord, il convient d'ajouter la référence à l'assembly System.Design.dll. Ensuite, toutes les classes mentionnées dans les sections qui suivent, appartiennent à l'espace de nom System.Windows.Forms.Design.Behavior.

Adorners

Un Adorner est un ensemble d'éléments graphiques qui peuvent être activés ou désactivés ensemble. S'ils sont désactivés, aucun dessin ne sera réalisé et aucun hit test ne sera fait pour savoir si la souris se trouve sur un des glyphes de l'Adorner désactivé. De plus, la classe Adorner possède une propriété BehaviorService de sorte que l'Adorner connaît son service Behavior. Enfin, on peut appeler la méthode Invalidate de l'Adorner afin de forcer le rafraîchissement du dessin de tous ses glyphes.

La classe Adorner possède les membres suivants :

  • La méthode Invalidate permet de forcer tous les glyphes de l'Adorner à se redessiner suivant un rectangle ou une région donnée ou encore en totalité
  • La propriété BehaviorService donne accès au service de comportement auquel l'Adorner est associé
  • La propriété Enabled permet d'activer (afficher) ou de désactiver (cacher) tous les glyphs de l'Adorner
  • La propriété Glyphs renvoie une collection contenant l'ensemble des glyphs qui font partie de l'Adorner.

Créer un Glyph

Vous devez créer une classe dérivée de Glyph afin d'implémenter le dessin de vos glyphes . La classe Glyph demande un Behavior mais si votre Glyph n'a pas d'interaction vous pouvez lui passer null/Nothing.

La classe Glyph possède les propriétés suivantes que vous pouvez redéfinir :

  • Bounds : renvoie la taille et la position auxquelles se trouve le glyph
  • Behavior : renvoie le Behavior associé au Glyph (par défaut renvoie l'instance passée au constructeur du glyph).

Vous devez redéfinir au moins les membres suivants :

  • GetHitTest : permet de renvoyer le curseur qui doit être affiché pour un point donné (en paramètre)
  • Paint : permet de dessiner le glyph sur le Graphics passé dans une propriété du paramètre « e ». Le glyph doit normalement être dessiné dans ses Bounds.

En général, on redéfinira Bounds aussi afin de spécifier le cadreduglyph.

/// <summary>
/// Notre glyph "texte central" : élément graphique (interactif en l'occurence)
/// dessinant un texte au milieu du contrôle
/// </summary>
private class CenterTextGlyph : Glyph
{
    //le contrôle sur lequel on dessine
    private Control myControl = null;
    
    //le service de comportement pour pouvoir par exemple, forcer le redessin du glyph
    private BehaviorService behaviorSvc = null;

    /// <summary>
    /// Construit le glyph avec les éléments dont il aura besoin.
    /// Le constructeur parent prend en paramètre le "comportement" associé au glyph :
    /// on lui passe donc une instance de notre classe dérivée de Behavior
    /// qui gère l'affichage du message au clic sur le texte
    /// </summary>
    /// <param name="control"></param>
    /// <param name="behaviorService"></param>
    public CenterTextGlyph(Control control, BehaviorService behaviorService)
        : base(newCenterTextBehavior())
    {
        this.myControl = control;
        this.behaviorSvc = behaviorService;
    }

    /// <summary>
    /// Renvoie les bornes du glyph
    /// </summary>
    public override Rectangle Bounds
    {
        get
        {
            //récupère la position et la taille de notre contrôle sur la surface de Design
            Rectangle bounds = behaviorSvc.ControlRectInAdornerWindow(this.myControl);
           
            //calcule la taille de la chaîne "Margin"
            //avec le double de taille de la police du contrôle
            Font textFont = newFont("Arial", this.myControl.Font.SizeInPoints * 2);
            SizeF textSizeF = SizeF.Empty;
            using (Graphics g = this.myControl.CreateGraphics())
            {
                textSizeF = g.MeasureString("Margin", textFont);
            }

            //calcule la position du texte pour le centrer dans le contrôle
            return new Rectangle(
                bounds.Left + (int)(bounds.Width - textSizeF.Width) / 2,
                bounds.Top + (int)(bounds.Height - textSizeF.Height) / 2,
                (int)textSizeF.Width,
                (int)textSizeF.Height
                );
        }
    }

    /// <summary>
    /// Indique le curseur à afficher quand le pointeur de la souris
    /// se trouve au point p
    /// (en général le curseur peut être différent s'il se trouve ou non
    /// dans la surface du glyph)
    /// Cela permet de savoir indirectement si la souris se trouve sur le glyph.
    /// Dans notre cas, on change le curseur quand on se trouve sur le texte.
    /// </summary>
    /// <param name="p"></param>
    /// <returns></returns>
    public override Cursor GetHitTest(Point p)
    {
        if (this.Bounds.Contains(p))
            return Cursors.Hand;
        else
            return null;
    }

    /// <summary>
    /// Dessine le glyph : texte "Margin" au centre du contrôle en magenta foncé
    /// Attention : les coordonnées sont relatifs à la surface de Design.
    /// Il faut donc se référer aux Bounds du glyph calculées
    /// à partir des Bounds de notre contrôle
    /// telles que renvoyées par le BehaviorService
    /// </summary>
    /// <param name="pe"></param>
    public override void Paint(PaintEventArgs pe)
    {
        //dessine en magenta
        using (Pen p = newPen(Color.DarkMagenta))
        {
            //le texte "Margin" récupère le rectangle dans lequel on va placer le texte
            Rectangle r = this.Bounds;
            using (Brush b = newSolidBrush(Color.DarkMagenta))
            {
                //double de la police du contrôle
                Font f = newFont("Arial", this.myControl.Font.SizeInPoints * 2);
                pe.Graphics.DrawString("Margin", f, b, r.Left, r.Top);
            }
        }
    }
}

Créer un Behavior

Le Behavior associe un comportement à un glyph. On peut aussi ajouter directement un Behavior dans le BehaviorService . Par exemple, un Behavior peut être appelé pour un drag-drop ou un clic. Le Behavior actif se trouve au sommet de la pile des Behavior dans le BehaviorService puis une fois utilisé, il est dépilé.

La classe Behavior possède les membres suivants :

  • La méthode FindCommand permet de récupérer une MenuCommand afin de manipuler (désactiver) l'item de menu correspondant
  • OnDragEnter,OnDragLeave : appelé lorsque le drag entre et sort de la surface du glyph du Behavior (par exemple, poignée de redimensionnement)
  • OnDragOver,OnDragDrop : appelé lorsque le drag est au dessus de la surface du glyph du Behavior et lors du drop
  • OnQueryContinueDrag : appelé pour demander si le drag-drop doit se poursuivre
  • OnGiveFeedback : appelé pour donner un feedback sur le drop
  • OnLoseCapture : appelé par l'Adornerparent quand il perd la souris
  • OnMouseDown , OnMouseMove, OnMouseUp, OnMouseDoubleClick : appelé au clic, déplacement, lâché, doubleclic de souris sur le glyph associé à ce Behavior
  • OnMouseEnter, OnMouseLeave, OnMouseHover : appelé lorsque la souris entre, sort et survole le glyph associé au Behavior
  • La propriété Cursor : renvoie le curseur qui doit être affiché pour ce Behavior
  • La propriété DisableAllCommands : indique si l'on désactive les commandes de menu pendant l'exécution de ce Behavior.

On peut redéfinir chacune des méthodes et propriétés précédentes.

/// <summary>
/// Gère le comportement au clic sur le glyph associé
/// </summary>
private class CenterTextBehavior : Behavior
{
    /// <summary>
    /// On affiche juste un message au clic gauche
    /// </summary>
    /// <param name="g"></param>
    /// <param name="button"></param>
    /// <param name="mouseLoc"></param>
    /// <returns></returns>
    public override bool OnMouseDown(Glyph g, MouseButtons button, Point mouseLoc)
    {
        if (button == MouseButtons.Left)
            MessageBox.Show("Vous m'avez cliqué dessus ? Emplacement " +mouseLoc.ToString());

        return base.OnMouseDown(g, button, mouseLoc);
    }
}

Créer un Designer pour ajouter l'Adorner au Behavior Service

Il est nécessaire de créer un Designer dérivant de ControlDesigner et de redéfinir sa méthode Initialize afin d'ajouter un ou plusieurs Adorner s contenant un ou plusieurs Glyph au service Behavior :

  • On récupère une instance du BehaviorService à partir de la méthode GetService du ControlDesigner
  • On crée une instance de la classe Adorner
  • On ajoute à cette instance (méthode Add de la propriété Glyphs ) autant d'instances de dérivées de la classe Glyph que nous voulons dessiner de Glyphs
  • On ajoute l'instance d' Adorner à la liste des Adorners du BehaviorService récupéré (méthode Add de la propriété Adorners )

Cela se fera avec le code suivant :

//créer un "orneur" (Adorner) pour contenir la liste de nos glyphs
//en rapport avec les marges du contrôle
Adorner myAdorner = newAdorner();

//Ajoute les glyphs :
//-> un non interactif qui est le cadre autour des marges de notre contrôle
//-> un interactif qui permet d'afficher un message au clic
//      que le texte central qu'il affiche
myAdorner.Glyphs.Add(newMarginPaintGlyph(this.Control,behaviorSvc,compSvc));
myAdorner.Glyphs.Add(newCenterTextGlyph(this.Control, behaviorSvc));

//ajoute "l'orneur" au service de comportement (BehaviorService)
behaviorSvc.Adorners.Add(myAdorner);

Le BehaviorService possède les membres suivants :

  • AdornerWindowPointToScreen : convertit un point de coordonnées dans la couche d'ornement vers coordonnées écran
  • AdornerWindowToScreen : renvoie la position en coordonnées écran de la couche d'ornement
  • ControlRectInAdornerWindow : renvoie la position et la taille d'un contrôle sur la couche d'ornement de la surface de Design
  • ControlToAdornerWindow : renvoie la position d'un contrôle sur la couche d'ornement de la surface de Design
  • Invalidate : permet de forcer le rafraichissement des ornements d'un rectangle, d'une région ou en totalité
  • MapAdornerWindowPoint : convertit un point de coordonnées handle de fenêtre vers coordonnées dans la couche d'ornement
  • ScreenToAdornerWindow : convertit un point de coordonnées écran vers coordonnées dans la couche d'ornement
  • SyncSelection : synchronise tous les ornements de sélection
  • La propriété Adorners : renvoie la liste des ornements de la surface de Design
  • La propriété AdornerWindowGraphics : renvoie le Graphics de la couche d'ornement qui recouvre toute la surface de Design
  • La propriété CurrentBehavior et les méthodes PopBehavior / PushBehavior / PushCaptureBehavior / GetNextBehavior permettent de manipuler la pile des Behavior en cours : le premier Behavior de la pile est celui qui a le focus, la souris et sur lequel les méthodes OnXXX sont appelées.

Exemple complet

Voici un exemple montrant divers Adornments et comportements au Design :

/// <summary>
/// Démontre comment créer un contrôle avec des "ornements" interactifs
/// et non interactifs au moment du Design
/// L'attribut Designer permet de spécifier notre Designer personnalisé
/// ajoutant nos "ornements" au service de comportement
/// (BehaviorService). Un "ornement" est une instance de la classe Adorner
/// contenant une liste de "choses graphiques" ou "glyph"
/// (le dessin) qui ont un rapport commun. (par exemple, il y a un Adorner
/// contenant les 8 poignées (glyph) de redimensionnement)
/// </summary>
[DesignerAttribute(typeof(BehaviorUserControlDesigner))]
public partial class BehaviorUserControl : UserControl
{
    public BehaviorUserControl()
    {
        InitializeComponent();
    }


    /// <summary>
    /// Notre Designer ajoutant nos "adorners" et nos "glyphs"
    /// dans le service de comportement (BehaviorService)
    /// </summary>
    private class BehaviorUserControlDesigner : ControlDesigner
    {
        /// <summary>
        /// Appelé à l'initialisation du Designer
        /// </summary>
        /// <param name="component"> composant auquel on est attaché </param>
        public override void Initialize(IComponent component)
        {
            base.Initialize(component);

            //récupère le service de comportement qui permet d'ajouter
            //les adorners et les glyphs
            BehaviorService behaviorSvc = (BehaviorService) this.GetService(typeof(BehaviorService));
            
            //le service de changement des composants pour rafraîchir
            //la taille et position des glyphs
            //quand notre composant change ses marges
            IComponentChangeService compSvc = (IComponentChangeService) this.GetService(typeof(IComponentChangeService));
            
            //qui permet de savoir quel contrôle est actuellement sélectionné
            //et de savoir quand la sélection change
            ISelectionService selSvc = (ISelectionService)this.GetService(typeof(ISelectionService));

            if (behaviorSvc != null)
            {
                //créer un "orneur" (Adorner) pour contenir la liste de nos glyphs
                //en rapport avec les marges du contrôle
                Adorner myAdorner = newAdorner();
                
                //Ajoute les glyphs :
                //-> un non interactif qui est le cadre autour des marges
                //de notre contrôle
                //-> un interactif qui permet d'afficher un message au clic
                //que le texte central qu'il affiche
                myAdorner.Glyphs.Add(new MarginPaintGlyph(this.Control,behaviorSvc,compSvc));
                myAdorner.Glyphs.Add(newCenterTextGlyph(this.Control, behaviorSvc));
                
                //ajoute "l'orneur" au service de comportement (BehaviorService)
                behaviorSvc.Adorners.Add(myAdorner);

                //créer un "orneur" permettant de modifier l'ancrage du contrôle
                // (sa propriété Anchor)
                Adorner myAnchorAdorner = newAdorner();
                
                //Ajoute les glyphs des quatre bords permet d'activer l'ancrage
                //sur le bord correspondant
                myAnchorAdorner.Glyphs.Add(new AnchorGlyph(this.Control, behaviorSvc, compSvc, selSvc, myAnchorAdorner, AnchorGlyph.Border.Left));
                myAnchorAdorner.Glyphs.Add(new AnchorGlyph(this.Control, behaviorSvc, compSvc, selSvc, myAnchorAdorner, AnchorGlyph.Border.Right));
                myAnchorAdorner.Glyphs.Add(new AnchorGlyph(this.Control, behaviorSvc, compSvc, selSvc, myAnchorAdorner, AnchorGlyph.Border.Top));
                myAnchorAdorner.Glyphs.Add(new AnchorGlyph(this.Control, behaviorSvc, compSvc, selSvc, myAnchorAdorner, AnchorGlyph.Border.Bottom));
                
                //ajoute "l'orneur" au service de comportement (BehaviorService)
                behaviorSvc.Adorners.Add(myAnchorAdorner);
            }
        }
    }

    /// <summary>
    /// Notre glyph de marge : élément graphique (non intéractif en l'occurence)
    /// dessinant le cadre des marges du contrôle
    /// </summary>
    private class MarginPaintGlyph : Glyph
    {
        //le contrôle sur lequel on dessine
        private Control myControl = null;
        
        //le service de comportement pour pouvoir par exemple, forcer le redessin du glyph
        private BehaviorService behaviorSvc = null;
        
        //le service de changement de composant, pour pouvoir repositionner le glyph
        //quand il change ses marges
        private IComponentChangeService compSvc = null;

        /// <summary>
        /// Construit le glyph avec les éléments dont il aura besoin.
        /// Le constructeur parent attend une instance d'un comportement.
        /// Dans notre cas, on lui passe "null", car le glyph n'est pas intéractif
        /// </summary>
        /// <param name="control"></param>
        /// <param name="behaviorService"></param>
        /// <param name="compChangeService"></param>
        public MarginPaintGlyph(Control control, BehaviorService behaviorService, IComponentChangeService compChangeService)
            : base(null)
        {
            this.myControl = control;
            this.behaviorSvc = behaviorService;
            this.compSvc = compChangeService;

            //demande la notification quand un composant de la surface de Design change
            //et en particulier le nôtre
            if (compChangeService != null)
                compChangeService.ComponentChanged += new ComponentChangedEventHandler(compChangeService_ComponentChanged);
        }

        //au changement des marges, de la position ou de la taille de notre composant,
        //on doit redessiner le rectangle des marges qu'affiche ce glyph
        void compChangeService_ComponentChanged(object sender, ComponentChangedEventArgs e)
        {
            //si le composant modifié est le notre et que l'on modifie ses marges
            if (object.ReferenceEquals(e.Component, this.myControl)
                && (e.Member.Name == "Margin"
                    || e.Member.Name == "Size"
                    || e.Member.Name == "Location")
                )
            {
                //on doit redessiner le glyph pour rafraichir le rectangle des marges
                behaviorSvc.Invalidate();
            }
        }

        /// <summary>
        /// Indique le curseur à afficher quand le pointeur de la souris se trouve
        /// au point p
        /// (en général le curseur peut être différent s'il se trouve
        /// ou non dans la surface du glyph)
        /// Cela permet de savoir indirectement si la souris se trouve sur le glyph.
        /// Dans notre cas, on ne change pas le curseur quand on est dans le rectangle
        /// des marges
        /// puisque notre glyph n'est pas interactif
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        public override Cursor GetHitTest(Point p)
        {
            return null;
        }

        /// <summary>
        /// Renvoie les bornes du glyph
        /// </summary>
        public override Rectangle Bounds
        {
             get
            {
                //récupère la position et la taille de notre contrôle sur la surface de Design
                Rectangle bounds = behaviorSvc.ControlRectInAdornerWindow(this.myControl);

                 //ajoute les marges aux bornes du contrôle
                return Rectangle.FromLTRB(
                    bounds.Left   - this.myControl.Margin.Left,
                    bounds.Top    - this.myControl.Margin.Top,
                    bounds.Right  + this.myControl.Margin.Right,
                    bounds.Bottom + this.myControl.Margin.Bottom
                );
            }
        }

        /// <summary>
        /// Dessine le glyph : cadre des marges en magenta foncé
        /// Attention : les coordonnées sont relative à la surface de Design.
        /// Il faut donc se référer aux Bounds du glyph calculées à partir
        /// des Bounds de notre contrôle telles que renvoyées
        /// par le BehaviorService
        /// </summary>
        /// <param name="pe"></param>
        public override void Paint(PaintEventArgs pe)
        {
            //dessine en magenta
            using(Pen p = newPen(Color.DarkMagenta))
            {
                //un rectangle représentant les marges de notre contrôle
                Graphics g = pe.Graphics;
                g.DrawRectangle(p, this.Bounds);
            }
        }
    }

    /// <summary>
    /// Notre glyph "texte central" : élément graphique (intéractif en l'occurence)
    /// dessinant un texte au milieu du contrôle
    /// </summary>
    private class CenterTextGlyph : Glyph
    {
        //le contrôle sur lequel on dessine
        private Control myControl = null;
        
        //le service de comportement pour pouvoir par exemple, forcer le redessin du glyph
        private BehaviorService behaviorSvc = null;

        /// <summary>
        /// Construit le glyph avec les éléments dont il aura besoin.
        /// Le constructeur parent prend en paramètre le "comportement" associé au glyph :
        /// on lui passe donc une instance de notre classe dérivée de Behavior
        /// qui gère l'affichage du message au clic sur le texte
        /// </summary>
        /// <param name="control"></param>
        /// <param name="behaviorService"></param>
        public CenterTextGlyph(Control control, BehaviorService behaviorService)
            : base(newCenterTextBehavior())
        {
            this.myControl = control;
            this.behaviorSvc = behaviorService;
        }

        /// <summary>
        /// Renvoie les bornes du glyph
        /// </summary>
        public override Rectangle Bounds
        {
            get
            {
                //récupère la position et la taille de notre contrôle
                //sur la surface de Design
                Rectangle bounds = behaviorSvc.ControlRectInAdornerWindow(this.myControl);
               
                //calcule la taille de la chaîne "Margin" avec le double de la taille
                //de la police du contrôle
                Font textFont = newFont("Arial", this.myControl.Font.SizeInPoints * 2);
                SizeF textSizeF = SizeF.Empty;
                using (Graphics g = this.myControl.CreateGraphics())
                {
                    textSizeF = g.MeasureString("Margin", textFont);
                }

                //calcule la position du texte pour le centrer dans le contrôle
                return new Rectangle(
                        bounds.Left + (int)(bounds.Width - textSizeF.Width) / 2,
                        bounds.Top + (int)(bounds.Height - textSizeF.Height) / 2,
                        (int)textSizeF.Width,
                        (int)textSizeF.Height
                    );
            }
        }

        /// <summary>
        /// Indique le curseur à afficher quand le pointeur de la souris
        /// se trouve au point p
        /// (en général le curseur peut être différent s'il se trouve
        /// ou non dans la surface du glyph)
        /// Cela permet de savoir indirectement si la souris se trouve sur le glyph.
        /// Dans notre cas, on change le curseur quand on se trouve sur le texte.
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        public override Cursor GetHitTest(Point p)
        {
            if (this.Bounds.Contains(p))
                return Cursors.Hand;
            else
                return null;
        }

        /// <summary>
        /// Dessine le glyph : texte "Margin" au centre du contrôle en magenta foncé
        /// Attention : les coordonnées sont relatives à la surface de Design.
        /// Il faut donc se référer aux Bounds du glyph calculées à partir des Bounds
        /// de notre contrôle
        /// telles que renvoyées par le BehaviorService
        /// </summary>
        /// <param name="pe"></param>
        public override void Paint(PaintEventArgs pe)
        {
            //dessine en magenta
            using (Pen p = newPen(Color.DarkMagenta))
            {
                //le texte "Margin"
                //récupère le rectangle dans lequel on va placer le texte
                Rectangle r = this.Bounds;
                using (Brush b = newSolidBrush(Color.DarkMagenta))
                {
                    //double de la police du contrôle
                    Font f = newFont("Arial", this.myControl.Font.SizeInPoints * 2);
                    pe.Graphics.DrawString("Margin", f, b,
                            r.Left, r.Top);
                }
            }
        }

        /// <summary>
        /// Gère le comportement au clic sur le glyph associé
        /// </summary>
        private class CenterTextBehavior : Behavior
        {
            /// <summary>
            /// On affiche juste un message au clic gauche
            /// </summary>
            /// <param name="g"></param>
            /// <param name="button"></param>
            /// <param name="mouseLoc"></param>
            /// <returns></returns>
            public override bool OnMouseDown(Glyph g, MouseButtons button, Point mouseLoc)
            {
                if (button == MouseButtons.Left)
                    MessageBox.Show("Vous m'avez cliqué dessus ? Emplacement " +mouseLoc.ToString());

                return base.OnMouseDown(g, button, mouseLoc);
            }
        }
    }


    /// <summary>
    /// Notre glyph de définition des ancres : élément graphique
    /// (interactif en l'occurence)
    /// dessinant un rectangle sur chacun des bords du contrôle
    /// (rectangle rouge si une ancre est définie sur ce bord, vert sinon)
    /// </summary>
    private class AnchorGlyph : Glyph
    {
        //taille du glyph en position verticale (horizontale, on inverse)
        private const int glyphHeight = 30;
        private const int glyphWidth = 20;

        //le contrôle sur lequel on dessine
        private Control myControl = null;
        //le service de comportement pour pouvoir par exemple, forcer le redessin du glyph
        private BehaviorService behaviorSvc = null;
        //le service de changement de composant, pour pouvoir repositionner le glyph
        //quand le contrôle change de place ou de taille
        private IComponentChangeService compSvc = null;
        //permet de savoir quand la sélection change et quel contrôle est sélectionné
        private ISelectionService selectionService = null;
        //permet de ne redessiner/activer/désactiver que nos glyphs
        private Adorner myAdorner = null;

        //indique sur quel bord doit se trouver notre glyph d'ancre
        public enum Border
        {
            Left,
            Right,
            Top,
            Bottom
        }
        private Border border;

        /// <summary>
        /// Construit le glyph avec les éléments dont il aura besoin
        /// </summary>
        /// <param name="control"></param>
        /// <param name="behaviorService"></param>
        /// <param name="compChangeService"></param>
        /// <param name="selectionService"></param>
        /// <param name="parentAdorner"></param>
        /// <param name="border"></param>
        public AnchorGlyph(Control control, BehaviorService behaviorService, IComponentChangeService compChangeService, ISelectionService selectionService, Adorner parentAdorner, Border border)
            : base(newAnchorBehavior(control,parentAdorner,border))
        {
            this.myControl = control;
            this.behaviorSvc = behaviorService;
            this.compSvc = compChangeService;
            this.selectionService = selectionService;
            this.border = border;
            this.myAdorner = parentAdorner;

            //demande la notification quand un composant de la surface de Design change
            if (compChangeService != null)
                compChangeService.ComponentChanged += new ComponentChangedEventHandler(compChangeService_ComponentChanged);

            //demande la notification quand la sélection change
            if (selectionService != null)
                selectionService.SelectionChanged += new EventHandler(selectionService_SelectionChanged);
        }

        //permet d'activer le glyph uniquement quand notre contrôle est sélectionné
        // (comme sélection principale)
        void selectionService_SelectionChanged(object sender, EventArgs e)
        {
            //si le composant sélectionné est le nôtre
            if (object.ReferenceEquals(selectionService.PrimarySelection, this.myControl))
                //on affiche (active) nos glyphs
                this.myAdorner.Enabled = true;
            else
                //sinon on les cache
                this.myAdorner.Enabled = false;
        }

        //au changement de la position/taille/sa propriété Anchor de notre composant,
        //on doit redessiner le glyph sur son bord au bon endroit
        void compChangeService_ComponentChanged(object sender, ComponentChangedEventArgs e)
        {
            //si le composant modifié est le nôtre et que l'on modifie sa position
            //ou sa taille
            //ou sa propriété Anchor (changement éventuel de la couleur du glyph
            //associé au bord)
            if (object.ReferenceEquals(e.Component, this.myControl)
                && (e.Member.Name == "Anchor"
                    || e.Member.Name == "Size"
                    || e.Member.Name == "Location")
                )
            {
                //on doit redessiner le glyph pour rafraîchir le rectangle de
                //l'ancre et/ou sa couleur
                this.myAdorner.Invalidate();
            }
        }

        /// <summary>
        /// Renvoie les bornes du glyph
        /// </summary>
        public override Rectangle Bounds
        {
            get
            {
                //récupère la position et la taille de notre contrôle
                //sur la surface de Design
                Rectangle bounds = behaviorSvc.ControlRectInAdornerWindow(this.myControl);

                //place le contrôle au milieu de son bord affecté
                //note ; la largeur et la hauteur du rectangle de ce glyph
                //est inversé pour les bords horizontaux)
                switch (border)
                 {
                    case Border.Left:
                        return new Rectangle(
                                bounds.Left,
                                bounds.Top + (int)(bounds.Height - glyphHeight) / 2,
                                glyphWidth,
                                glyphHeight
                            );
                    case Border.Right:
                        return new Rectangle(
                                bounds.Right - glyphWidth,
                                bounds.Top + (int)(bounds.Height - glyphHeight) / 2,
                                glyphWidth,
                                glyphHeight
                            );
                     case Border.Top:
                        return new Rectangle(
                                bounds.Left + (int)(bounds.Width - glyphHeight) / 2,
                                bounds.Top,
                                glyphHeight,
                                glyphWidth
                            );
                    case Border.Bottom:
                        return new Rectangle(
                                bounds.Left + (int)(bounds.Width - glyphHeight) / 2,
                                bounds.Bottom - glyphWidth,
                                glyphHeight,
                                glyphWidth
                            );
                    default:
                         return Rectangle.Empty;
                }
            }
        }

        /// <summary>
        /// Indique le curseur à afficher quand le pointeur de la souris
        /// se trouve au point p
        /// (en général le curseur peut être différent s'il se trouve
        /// ou non dans la surface du glyph)
        /// Cela permet de savoir indirectement si la souris se trouve sur le glyph.
        /// Dans notre cas, on change le curseur quand on se trouve
        /// sur le rectangle d'ancre.
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        public override Cursor GetHitTest(Point p)
        {
            if (this.Bounds.Contains(p))
                return Cursors.Hand;
            else
                return null;
        }

        /// <summary>
        /// Dessine le glyph : rectangle sur un bord du contrôle
        /// (rouge si l'ancre est définie pour ce bord, vert sinon)
        /// Attention : les coordonnées sont relatives à la surface de Design.
        /// Il faut donc se référer aux Bounds du glyph calculées à partir
        /// des Bounds de notre contrôle
         /// telles que renvoyées par le service BehaviorService
        /// </summary>
        /// <param name="pe"></param>
        public override void Paint(PaintEventArgs pe)
        {
            Color bg = (((AnchorBehavior)this.Behavior).AnchorForBorder ? Color.Red : Color.Green);
            //dessine un rectangle de la couleur bg avec bordure noire
            using (Pen p = newPen(Color.Black))
            {
                //récupère le rectangle dans lequel on va dessiner
                Rectangle r = this.Bounds;
                using (Brush b = newSolidBrush(bg))
                {
                    pe.Graphics.FillRectangle(b, r);
                }
                pe.Graphics.DrawRectangle(p, r);
            }
        }

        /// <summary>
        /// Définit le comportement de modification de l'ancre associée
        /// au bord du glyph associé...
        /// </summary>
        private class AnchorBehavior : Behavior
        {
            //le contrôle sur lequel on dessine
            private Control myControl = null;
            //adorner parent pour rafraîchir les quatre glyphs au changement
            //de la propriété Anchor par ce Behavior
            private Adorner myAdorner = null;
            //sur quel bord du contrôle sommes-nous ?
            private Border border;

            public AnchorBehavior(Control myControl, Adorner parentAdorner, Border border)
            {
                this.border = border;
                this.myAdorner = parentAdorner;
                this.myControl = myControl;
             }

            //renvoie l'AnchorStyles associé au bord sur lequel est le glyph
            //qui a ce Behavior
            private AnchorStyles getAnchorStyleForBorder()
            {
                AnchorStyles ancStyle = AnchorStyles.None;
                switch (this.border)
                {
                    case Border.Left:
                        ancStyle = AnchorStyles.Left;
                        break;
                    case Border.Right:
                        ancStyle = AnchorStyles.Right;
                        break;
                    case Border.Top:
                        ancStyle = AnchorStyles.Top;
                        break;
                     case Border.Bottom:
                        ancStyle = AnchorStyles.Bottom;
                        break;
                }
                return ancStyle;
            }

            /// <summary>
            /// Définir la propriété Anchor en fonction de la valeur de ce Behavior
            /// et de son glyph
            /// Renvoie true si une ancre est présente sur le bord
            /// auquel ce Behavior est associé
            /// </summary>
            public bool AnchorForBorder
            {
                get
                {
                    //renvoie true si un Anchor est défini pour le bord associé
                    //à ce Behavior
                    AnchorStyles ancStyle = getAnchorStyleForBorder();
                    return (this.myControl.Anchor & ancStyle) == ancStyle;
                }
                set
                {
                    //récupère la propriété Anchor
                    PropertyDescriptor propAnchor = TypeDescriptor.GetProperties(this.myControl)["Anchor"];
                    //récupère le AnchorStyles de notre bord
                    AnchorStyles ancStyle = getAnchorStyleForBorder();
                    //lit directement la valeur de la propriété Anchor
                    AnchorStyles anchorValue = this.myControl.Anchor;
                    //active ou désactive l'ancre pour notre bord
                    if (value)
                    {
                        anchorValue |= ancStyle;
                    }
                    else
                    {
                        anchorValue &= ~ancStyle;
                    }
                    //définit la valeur en avertissant le service IComponentChangeService
                    //et donc redessine les glyphs du contrôle
                    propAnchor.SetValue(this.myControl, anchorValue);
                }
            }

            public override bool OnMouseDown(Glyph g, MouseButtons button, Point mouseLoc)
            {
                //toggle la définition de l'ancre pour notre bord
                this.AnchorForBorder = !this.AnchorForBorder;
                return base.OnMouseDown(g, button, mouseLoc);
            }
        }
    }
 }

Références

Behavior Service Overview

Ce document intitulé « Behavior Service : la nouvelle méthode d'ornement au Design Time » 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.