Accueil
AutoLISP
.NET
Tutoriels
Liens
Contact

Les interfaces utilisateur avec AutoCAD .NET






Introduction



Avant propos

On ne s'attardera pas sur l'(in)utilité de ce que réalisent les exemples dans AutoCAD, une tâche simple a été sciemment choisie pour qu'elle ne brouille pas trop le propos, à savoir le passage des données entre les différents composants du programme.
La tâche consiste simplement à dessiner un cercle sur le point spécifié en fonction du calque et du rayon précédemment renseignés par l'utilisateur.

Chaque exemple présentera une interface utilisateur différente, ajoutant progressivement un peu de complexité par rapport à l'exemple précédent.

Les exemples supposent, au minimum, une connaissance des base de la programmation .NET et ne s'attardent pas sur les détails de création des interfaces dans Visual Studio, ils mettent plutôt l'accent sur les interactions entre l'interface et l'application.

Les codes utilisent certaines nouveautés syntaxiques de C# 6 (Visual Studio 2015).



Ligne de commande

Commençons par la ligne de commande, interface de base pour récupérer les entrées utilisateur.

Rien de particulier ici, il s'agit d'une simple commande AutoCAD utilisant des objets dérivant de PromptEditorOptions et PromptResult pour récupérer les entrées utilisateur.
Tout se passe dans la même classe, voire dans même méthode.

On notera juste que les commandes AutoCAD sont définies dans une classe et qu'une seule instance de cette classe est crée pour chaque document au premier appel d'une commande (si celle-ci est définie par une méthode non statique). Ceci permet d'utiliser des membres de cette classe (champ ou propriétés) pour stocker des données par document comme les valeurs par défaut du calque et du rayon utilisées par notre commande.

Une méthode privée : GetLayerNames() est utilisée pour collecter les noms de calque du document actif afin de procéder à certaines validations.

Commands.cs
using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Core.Application;

[assembly: CommandClass(typeof(AcadUISample.CommandLine.Commands))]

namespace AcadUISample.CommandLine
{
    public class Commands
    {
        // champs d'instance
        Document doc;  // document actif
        double radius; // valeur par défaut pour le rayon
        string layer;  // valeur par défaut pour le calque
        
        /// <summary>
        /// Crée une instance de Commands.
        /// Ce constructeur est appelé une fois par document 
        /// au premier appel d'une méthode 'CommandMethod'.
        /// </summary>
        public Commands()
        {
            // initialisation des champs privés (valeurs par défaut initiales)
            doc = AcAp.DocumentManager.MdiActiveDocument;
            radius = 10.0;
            layer = (string)AcAp.GetSystemVariable("clayer");
        }
        
        /// <summary>
        /// Commande de dessin du cercle
        /// </summary>
        [CommandMethod("CMD_CIRCLE")]
        public void DrawCircleCmd()
        {
            var db = doc.Database;
            var ed = doc.Editor;
            
            // choix du calque
            var layers = GetLayerNames(db);
            if (!layers.Contains(layer))
            {
                layer = (string)AcAp.GetSystemVariable("clayer");
            }
            var strOptions = new PromptStringOptions("\nNom du calque: ");
            strOptions.DefaultValue = layer;
            strOptions.UseDefaultValue = true;
            var strResult = ed.GetString(strOptions);
            if (strResult.Status != PromptStatus.OK)
                return;
            if (!layers.Contains(strResult.StringResult))
            {
                ed.WriteMessage(
                  $"\nAucun calque nommé '{strResult.StringResult}' dans la table des calques.");
                return;
            }
            layer = strResult.StringResult;
            
            // spécification du rayon
            var distOptions = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            distOptions.DefaultValue = radius;
            distOptions.UseDefaultValue = true;
            var distResult = ed.GetDistance(distOptions);
            if (distResult.Status != PromptStatus.OK)
                return;
            radius = distResult.Value;
            
            // spécification du centre
            var ppr = ed.GetPoint("\nSpécifiez le centre: ");
            if (ppr.Status == PromptStatus.OK)
            {
                // dessin du cercle dans l'espace courant
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace =
                      (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = strResult.StringResult;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }
        
        /// <summary>
        /// Obtient la liste des calques.
        /// </summary>
        /// <param name="db">Instance de Database à laquelle s'applique la méthode.</param>
        /// <returns>Liste des noms de claques.</returns>
        private List<string> GetLayerNames(Database db)
        {
            var layers = new List<string>();
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                var layerTable = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
                foreach (ObjectId id in layerTable)
                {
                    var layer = (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead);
                    layers.Add(layer.Name);
                }
            }
            return layers;
        }
    }
}
        

Retour au sommaire.





Avec Windows Forms (WinForms)



Boite de dialogue modale

Les boites dialogues peuvent être affichées suivant deux modes : modal ou non modal.

Dans AutoCAD, on affiche les boites de dialogue modales avec la méthode : Application.ShowModalDialog().
L'utilisateur n'a alors plus accès à AutoCAD tant que la boite de dialogue n'est pas masquée ou fermée.

La méthode Application.ShowModalDialog() requiert comme paramètre une instance du formulaire à afficher et retourne un objet de type DialogResult (il s'agit d'une énumération) qui indique comment la boite de dialogue a été fermée.
Typiquement, on affecte à la propriété DialogResult de deux boutons : "OK" et "Annuler" les valeurs DialogResult.OK et DialogResult.Cancel. On voit donc comment la méthode qui affiche la boite de dialogue pourra récupérer du même coup une valeur indiquant si l'utilisateur l'a fermée en cliquant sur "OK" ou sur "Annuler".

Mais en général le projet définit au moins deux classes : une classe dans laquelle est définie la commande AutoCAD pour afficher la boite de dialogue et pour agir dans AutoCAD en fonction du résultat et une autre classe dans laquelle est définie la boite de dialogue elle même.
Il faut pouvoir passer des données d'une classe à l'autre.

Pour passer la listes des calques du dessin courant et les valeurs par défaut (calque et rayon) de la commande à la boite de dialogue, on peut utiliser les paramètres du constructeur du formulaire (on surchargera celui-ci en conséquence).
La liste des noms de calque sera liée à la liste déroulante du formulaire comme source de données.

Et pour passer les valeurs spécifiées par l'utilisateur dans la boite de dialogue à la commande on peut définir des propriétés dans la classe du formulaire dont les valeurs seront accessibles via l'instance du formulaire créée dans la commande.

Commands.cs
using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.ModalDialog.Commands))]

namespace AcadUISample.ModalDialog
{
    public class Commands
    {
        // champs d'instance
        Document doc;  // document actif
        double radius; // valeur par défaut pour le rayon
        string layer;  // valeur par défaut pour le calque
        
        /// <summary>
        /// Crée une instance de Commands
        /// Ce constructeur est appelé une fois par document 
        /// au premier appel d'une méthode 'CommandMethod'.
        /// </summary>
        public Commands()
        {
            // initialisation des champs privés (valeurs par défaut initiales)
            doc = AcAp.DocumentManager.MdiActiveDocument;
            layer = (string)AcAp.GetSystemVariable("clayer");
            radius = 10.0;
        }
        
        /// <summary>
        /// Commande d'affichage de la boite de dialogue
        /// </summary>
        [CommandMethod("CMD_MODAL")]
        public void ModalDialogCmd()
        {
            // création d'une instance de ModalDialog avec les données du document
            // (calques) et les valeurs par défaut
            var layers = GetLayerNames(doc.Database);
            if (!layers.Contains(layer))
            {
                layer = (string)AcAp.GetSystemVariable("clayer");
            }
            using (var dialog = new ModalDialog(layers, layer, radius))
            {
                // affichage modal de la boite de dialogue et action en fonction
                // de la valeur de DialogResult
                var dlgResult = AcAp.ShowModalDialog(dialog);
                if (dlgResult == System.Windows.Forms.DialogResult.OK)
                {
                    // mise à jour des champs
                    layer = dialog.Layer;
                    radius = dialog.Radius;
                    // dessin du cercle
                    DrawCircle(radius, layer);
                }
            }
        }
        
        /// <summary>
        /// Dessine un cercle.
        /// </summary>
        /// <param name="radius">Rayon du cercle.</param>
        /// <param name="layer">Calque du cercle.</param>
        private void DrawCircle(double radius, string layer)
        {
            var db = doc.Database;
            var ed = doc.Editor;
            var ppr = ed.GetPoint("\nSpécifiez le centre: ");
            if (ppr.Status == PromptStatus.OK)
            {
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = layer;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }
        
        /// <summary>
        /// Obtient la liste des calques.
        /// </summary>
        /// <param name="db">Instance de Database à laquelle s'applique la méthode.</param>
        /// <returns>Liste des noms de claques.</returns>
        private List<string> GetLayerNames(Database db)
        {
            var layers = new List<string>();
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                var layerTable = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
                foreach (ObjectId id in layerTable)
                {
                    var layer = (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead);
                    layers.Add(layer.Name);
                }
            }
            return layers;
        }
    }
}
            
ModalDialog.cs
using Autodesk.AutoCAD.EditorInput;
using System.Collections.Generic;
using System.Windows.Forms;
using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.ModalDialog
{
    public partial class ModalDialog : Form
    {
        double radius;
        
        /// <summary>
        /// Obtient la valeur du rayon.
        /// </summary>
        public double Radius => radius;
        
        /// <summary>
        /// Obtient l'élément sélectionné dans le contrôle ComboBox.
        /// </summary>
        public string Layer => (string)cbxLayer.SelectedItem;
        
        /// <summary>
        /// Crée une nouvelle instance de ModalDialog.
        /// </summary>
        /// <param name="layers">Collection des calques à lier au contrôle ComboBox.</param>
        /// <param name="clayer">Nom du calque par défaut.</param>
        /// <param name="radius">Rayon par défaut.</param>
        public ModalDialog(List<string> layers, string clayer, double radius)
        {
            InitializeComponent();
            
            // affectation de valeur de DialogResult aux boutons
            btnCancel.DialogResult = DialogResult.Cancel;
            btnOk.DialogResult = DialogResult.OK;
            
            // liaison de la collection de calques au contrôle ComboBox
            cbxLayer.DataSource = layers;
            cbxLayer.SelectedItem = clayer;
            
            // valeur par défaut du rayon
            txtRadius.Text = radius.ToString();
        }
        
        /// <summary>
        /// Gestionnaire de l'événement 'TextChanged' de la boite de texte 'Rayon'.
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void txtRadius_TextChanged(object sender, System.EventArgs e)
        {
            // le bouton OK est 'grisé' si le texte ne représente pas un nombre valide
            // le champ radius est mis à jour en conséquence
            btnOk.Enabled = double.TryParse(txtRadius.Text, out radius);
        }
        
        /// <summary>
        /// Gestion de l'événement 'Click' du bouton 'Rayon'.
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void btnRadius_Click(object sender, System.EventArgs e)
        {
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            
            // AutoCAD masque automatiquement la boite de dialogue 
            // pour laisser la main à l'utilisateur
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                txtRadius.Text = pdr.Value.ToString();
        }
    }
}
            

Retour au sommaire.




Boite de dialogue non modale

Contrairement aux boites de dialogue modales, les boites de dialogue non modales ne monopolisent pas le focus quand elle sont affichées.
Elles prennent le focus automatiquement quand le curseur entre dans leur aire, mais ne le rendent pas automatiquement à AutoCAD quand le curseur en sort.
De plus, elles s'exécutent dans le contexte de l'application AutoCAD contrairement aux boites modales qui s'exécutent dans le contexte du document actif.
C'est donc au programme qu'incombe la responsabilité de s'occuper de certains passages de focus, de masquer la boite si nécessaire, de verrouiller le document actif avant de pouvoir le modifier et aussi de gérer les éventuelles modifications des données liées à l'interface dans le document ainsi que les changements de document actifs.

Pour notre exemple, ce dernier point nous importe en ce qui concerne la liste des calques qui peut être modifiée alors que la boite de dialogue est affichée ou encore, être différente d'un document à l'autre.
AutoCAD fournit l'API UIBindings qui sert justement à lier des des données AutoCAD avec les interfaces utilisateurs, notamment certaines collections comme celle des calques.
Les collections DataItemCollection obtenues avec UIBindings sont dites "observables", elles implémentent plusieurs interfaces dont INotitfyCollectionChanged qui, comme son nom l'indique, notifie des changements survenus dans la collection à l'interface utilisateur à laquelle elle est liée. On aurait bien sûr pu utiliser cette API aussi avec la boite de dialogue modale.

Pour afficher une boite de dialogue non modale dans AutoCAD, on utilise la méthode Application.ShowModelessDialog() qui requiert comme argument un formulaire, mais, dans ce cas, ne retourne rien (void).
Avec ce type de boite de dialogue, la commande ne peut qu'afficher la boite, ce sont les gestionnaires d'événement des différents contrôles qui interagiront avec AutoCAD.

Commands.cs
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.Modeless.Commands))]

namespace AcadUISample.Modeless
{
    public class Commands
    {
        /// <summary>
        /// Commande d'affichage de la boite de dialogue
        /// </summary
        [CommandMethod("CMD_MODELESS")]
        public static void ShowModelessDialog()
        {
            // création d'une instance de ModelessDialog
            var dialog = new ModelessDialog();

            // affichage non-modal de la boite de dialogue
            AcAp.ShowModelessDialog(dialog);
        }
    }
}
            
ModelessDialog.cs
using System;
using System.Linq;
using System.Windows.Forms;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Windows.Data;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.Modeless
{
    public partial class ModelessDialog : Form
    {
        // champs privés
        double radius;             // rayon du cercle
        DataItemCollection layers; // collection des données de calque
        
        /// <summary>
        /// Crée une nouvelle instance de ModelessDialog.
        /// Définit la liaison de données pour le contrôle ComboBox.
        /// </summary>
        public ModelessDialog()
        {
            InitializeComponent();
            layers = AcAp.UIBindings.Collections.Layers;
            
            // liaison entre le contrôle Combobox et les données de calque
            BindData();
            
            // mise à jour du contrôle ComboBox quand la collection des données de calque change.
            layers.CollectionChanged += (s, e) => BindData();
            
            // valeur par défaut du rayon
            txtRadius.Text = "10";
        }
        
        /// <summary>
        /// Gestionnaire de l'événement 'TextChanged' de la boite de texte 'Rayon'.
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void txtRadius_TextChanged(object sender, EventArgs e)
        {
            // le bouton OK est 'grisé' si le texte ne représente pas un nombre valide
            // le champ radius est mis à jour en conséquence
            btnOk.Enabled = double.TryParse(txtRadius.Text, out radius);
        }
        
        /// <summary>
        /// Gestionnaire de l'événement 'Click' sur le bouton 'Rayon'
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void btnRadius_Click(object sender, EventArgs e)
        {
            // masquer la boite de dialogue
            this.Hide();
            
            // inviter l'utilisateur à spécifier une distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
            {
                txtRadius.Text = pdr.Value.ToString();
            }
            
            // afficher la boite de dialogue
            this.Show();
        }
        
        /// <summary>
        /// Gestionnaire de l'événement 'Click' sur le bouton 'OK'
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void btnOk_Click(object sender, EventArgs e)
        {
            var doc = AcAp.DocumentManager.MdiActiveDocument;
            if (doc != null)
            {
                // passer le focus à l'éditeur d'AutoCAD 
                // avant AutoCAD 2015, utiliser : 
                // Autodesk.AutoCAD.Internal.Utils.SetFocusToDwgView();
                AcAp.MainWindow.Focus();
                
                // verrouiller le document
                using (doc.LockDocument())
                {
                    // dessiner le cercle
                    DrawCircle(doc, radius, (string)cbxLayer.SelectedItem);
                }
            }
        }
        
        /// <summary>
        /// Dessine un cercle.
        /// </summary>
        /// <param name="doc">Instance de Document à laquelle s'applique la méthode.</param>
        /// <param name="radius">Rayon du cercle.</param>
        /// <param name="layer">Calque du cercle.</param>
        private void DrawCircle(Document doc, double radius, string layer)
        {
            var db = doc.Database;
            var ed = doc.Editor;
            var ppr = ed.GetPoint("\nSpécifiez le centre: ");
            if (ppr.Status == PromptStatus.OK)
            {
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = layer;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }
        
        /// <summary>
        /// Définit les liaisons de données du contrôle Combobox
        /// </summary>
        private void BindData()
        {
            // liaison à la source de données 
            // l'objet DataItemCollection est transformé en un tableau de noms de calque
            cbxLayer.DataSource = layers.Select(x => ((INamedValue)x).Name).ToArray();
            
            // sélection du calque courant
            cbxLayer.SelectedItem = ((INamedValue)layers.CurrentItem)?.Name;
        }
    }
}
            

Retour au sommaire.




Palette

Les boites de dialogue comme ci-dessus ne sont pas (plus) utilisée par AutoCAD qui lui préfère les palettes (ou plus précisément les jeux de palettes : PaletteSet).
L'exemple précédent sert surtout à illustrer la gestion d'un formulaire non modal.

Une palette (ou un jeu de palettes) est une interface non modale, ancrable et repliable qui peut contenir plusieurs onglets (les palettes proprement dites).
C'est un objet propre à AutoCAD dont la création est un tout petit peu moins simple que celle d'un formulaire Windows.

Comme on le fait avec les formulaires Windows, il est d'usage de créer un classe qui dérive de PaletteSet pour définir l'apparence et les contrôles contenus dans le jeu de palettes.

Chaque onglet de la palette est une instance de System.Windows.Forms.UserControl qui est ajouté au jeu de palettes.

Comme avec l'exemple précédent la collection des calques est liée à une instance de DataItemCollection mais chaque élément de la liste déroulante est "dessiné" de façon à afficher une pastille carrée de la couleur du calque à côte du nom du calque. Encore une fois, ceci aurait pu aussi être fait avec les exemples précédents.

Pour simplifier (et sécuriser) l'accès au document courant, une commande semblable à celle définie dans l'exemple "Ligne de commande" est appelée depuis le gestionnaire d'événement du bouton OK via la méthode SendStringToExecute(). Ceci permet de laisser le soin à AutoCAD de verrouiller le document actif et de lui passer le focus.
En prime, on peut rappeler la commande par Entrée et valider les valeurs par défaut de même.

Commands.cs
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

[assembly: CommandClass(typeof(AcadUISample.Palette.Commands))]

namespace AcadUISample.Palette
{
    public class Commands
    {
        // champ statique
        static CustomPaletteSet palette;

        // champs d'instance (valeurs par défaut)
        double radius = 10.0;
        string layer;

        /// <summary>
        /// Commande d'affichage de la palette
        /// </summary>
        [CommandMethod("CMD_PALETTE")]
        public void ShowPaletteSet()
        {
            // création de la palette au premier appel de la commande
            if (palette == null)
                palette = new CustomPaletteSet();
            palette.Visible = true;
        }

        /// <summary>
        /// Commande de dessin du cercle
        /// </summary>
        [CommandMethod("CMD_CIRCLE_PALETTE", CommandFlags.Modal)]
        public void DrawCircleCmd()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            var db = doc.Database;
            var ed = doc.Editor;

            // choix du calque
            if (string.IsNullOrEmpty(layer))
                layer = (string)Application.GetSystemVariable("clayer");
            var strOptions = new PromptStringOptions("\nNom du calque: ");
            strOptions.DefaultValue = layer;
            strOptions.UseDefaultValue = true;
            var strResult = ed.GetString(strOptions);
            if (strResult.Status != PromptStatus.OK)
                return;
            layer = strResult.StringResult;

            // spécification du rayon
            var distOptions = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            distOptions.DefaultValue = radius;
            distOptions.UseDefaultValue = true;
            var distResult = ed.GetDistance(distOptions);
            if (distResult.Status != PromptStatus.OK)
                return;
            radius = distResult.Value;

            // spécification du centre
            var ppr = ed.GetPoint("\nSpécifiez le centre: ");
            if (ppr.Status == PromptStatus.OK)
            {
                // dessin du cercle dans l'espace courant
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, distResult.Value))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = strResult.StringResult;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }
    }
}
            
CustomPaletteSet.cs
using System;

using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.Windows;

namespace AcadUISample.Palette
{
    internal class CustomPaletteSet : PaletteSet
    {
        // champs statiques
        static bool wasVisible;

        /// <summary>
        /// Crée une nouvelle instance de CustomPaletteSet
        /// </summary>
        public CustomPaletteSet()
            : base("Palette", "CMD_PALETTE", new Guid("{1836C7AC-C70E-4CF7-AA05-F6298D275046}"))
        {
            Style =
                PaletteSetStyles.ShowAutoHideButton |
                PaletteSetStyles.ShowCloseButton |
                PaletteSetStyles.ShowPropertiesMenu;
            MinimumSize = new System.Drawing.Size(250, 150);
            Add("Cercle", new PaletteTab());

            // masquage automatique de la palette quand aucune instance de Document
            // n'est active (no document state)
            var docs = Application.DocumentManager;
            docs.DocumentBecameCurrent += (s, e) => 
                Visible = e.Document == null ? false : wasVisible;
            docs.DocumentCreated += (s, e) => 
                Visible = wasVisible;
            docs.DocumentToBeDeactivated += (s, e) => 
                wasVisible = Visible;
            docs.DocumentToBeDestroyed += (s, e) =>
            {
                wasVisible = Visible;
                if (docs.Count == 1)
                    Visible = false;
            };
        }
    }
}
      
PaletteTab.cs
using System;
using System.Drawing;
using System.Windows.Forms;

using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Windows.Data;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.Palette
{
    /// <summary>
    /// Décrit l'onglet (palette) d'un jeu de palettes (PaletteSet)
    /// </summary>
    public partial class PaletteTab : UserControl
    {
        // champs d'instance
        double radius;
        DataItemCollection layers; // collection des données de calque
        
        /// <summary>
        /// Crée une nouvelle instance de PaletteTab.
        /// Définit la liaison de données pour le contrôle ComboBox.
        /// </summary>
        public PaletteTab()
        {
            InitializeComponent();
            layers = layers = AcAp.UIBindings.Collections.Layers; ;
            
            // liaison entre le contrôle Combobox et les données de calque
            BindData();
            
            // mise à jour du contrôle ComboBox quand la collection des données de calque change.
            layers.CollectionChanged += (s, e) => BindData();
            
            // valeur par défaut du rayon
            txtRadius.Text = "10";
        }
        
        /// <summary>
        /// Gestionnaire d'événement 'TextChanged' de la boite de texte 'Rayon'.
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void txtRadius_TextChanged(object sender, EventArgs e)
        {
            // le bouton OK est 'grisé' si le texte ne représente pas un nombre valide
            // le champ radius est mis à jour en conséquence
            btnOk.Enabled = double.TryParse(txtRadius.Text, out radius);
        }
        
        /// <summary>
        /// Gestionnaire d'événement 'Click' sur le bouton 'Rayon'
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void btnRadius_Click(object sender, EventArgs e)
        {
            // passer le focus à l'éditeur d'AutoCAD 
            // avant AutoCAD 2015, utiliser : 
            // Autodesk.AutoCAD.Internal.Utils.SetFocusToDwgView();
            AcAp.MainWindow.Focus();
            
            // inviter l'utilisateur à spécifier une distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                txtRadius.Text = pdr.Value.ToString();
        }
        
        /// <summary>
        /// Gestionnaire d'événement 'Click' sur le bouton 'OK'
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void btnOk_Click(object sender, EventArgs e)
        {
            // appel de la commande 'CMD_CIRCLE' avec le calque et le rayon
            AcAp.DocumentManager.MdiActiveDocument?.SendStringToExecute(
                $"CMD_CIRCLE_PALETTE \"{((INamedValue)cbxLayer.SelectedItem).Name}\" {radius} ", 
                false, false, false);
        }
        
        /// <summary>
        /// Définit les liaisons de données du contrôle Combobox
        /// </summary>
        private void BindData()
        {
            // liaison à la source de données
            cbxLayer.DataSource = new BindingSource(layers, null);
            
            // définition de l'apparence des éléments de la liste déroulante
            cbxLayer.DrawMode = DrawMode.OwnerDrawFixed;
            cbxLayer.DrawItem += (_, e) =>
            {
                if (e.Index > -1)
                {
                    // recupération de l'élément et de ses propriétés
                    var item = layers[e.Index];
                    var properties = item.GetProperties();
                    
                    // dessin d'un carré de la couleur du calque
                    var color = (Autodesk.AutoCAD.Colors.Color)properties["Color"].GetValue(item);
                    var bounds = e.Bounds;
                    int height = bounds.Height;
                    Graphics graphics = e.Graphics;
                    e.DrawBackground();
                    var rect = new Rectangle(
                      bounds.Left + 4, bounds.Top + 4, height - 8, height - 8);
                    graphics.FillRectangle(new SolidBrush(color.ColorValue), rect);
                    graphics.DrawRectangle(new Pen(Color.Black), rect);
                    
                    // écriture du nom du calque
                    graphics.DrawString(
                        (string)properties["Name"].GetValue(item), 
                        e.Font,
                        new SolidBrush(e.ForeColor), bounds.Left + height, bounds.Top + 1);
                    e.DrawFocusRectangle();
                }
            };

            // sélection du calque courant
            cbxLayer.SelectedItem = layers.CurrentItem;
        }
    }
}
            

Retour au sommaire.





Avec Windows Presentation Fundation (WPF)


Tous les exemple précédents utilisent la technologie WinForms.
WPF (Windows Presentation Fundation) est la "nouvelle technologie" (depuis Windows Vista et le Framework 3.0, quand même) de création d'interfaces graphiques.
L'utilisation du langage XAML permet de créer des interfaces graphiques riches un peu à la manière du HTML.

Une interface WPF minimale est définie à l'aide de deux fichiers :
- le fichier .xaml qui définit l'aspect de l'interface, les abonnements aux événements et les liaisons de données des contrôles ;
- le fichier .xaml.cs qui définit la logique d'interaction entre l'interface utilisateur et l'application (code behind).

Par défaut, à moins qu'on soit dans un projet de type Application WPF, Visual Studio 2015 ne propose pas, comme élément à ajouter, de "Fenêtre (WPF)". Mais on peut toujours ajouter un "Contrôle utilisateur (WPF)" et remplacer UserControl par Window dans les fichiers .xaml et .xaml.cs.
On peut aussi ajouter un attribut "Title" et quelques autres à la balise Window.
À ce stade, on peut "Exporter le modèle" (menu Fichier) comme "Modèle d'élément" pour que Visual Studio nous le propose à l'avenir.


Boite de dialogue modale

Comme mise en bouche, commençons par une simple boite de dialogue modale équivalente à celle de l'exemple "Boite de dialogue modale" précédente (WinForm).

La principale différence est l'utilisation du langage XAML pour la description de l'interface utilisateur dont l'architecture ressemble un peu à celle du HTML (la comparaison s'arrête là).
Les contrôles sont ici nommés pour pouvoir y accéder depuis le code behind. Les événements des contrôles sont abonnés à des gestionnaires d'événement situés dans le code behind.

Pour afficher une boite de dialogue modale WPF depuis AutoCAD, on utilise la méthode Application.ShowModalWindow() en lui passant une instance de System.Windows.Window créée avec un constructeur surchargé semblable à celui utilisé avec WinForms.

Commands.cs
using System.Collections.Generic;
using System.Linq;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.ModalWpf.Commands))]

namespace AcadUISample.ModalWpf
{
    public class Commands
    {
        // champs d'instance
        Document doc;
        Database db;
        Editor ed;
        double radius; // valeur par défaut pour le rayon
        string layer;  // valeur par défaut pour le calque
        
        /// <summary>
        /// Crée une instance de Commands
        /// Ce constructeur est appelé une fois par document 
        /// au premier appel d'une méthode 'CommandMethod'.
        /// </summary>
        public Commands()
        {
            // initialisation des champs privés
            doc = AcAp.DocumentManager.MdiActiveDocument;
            db = doc.Database;
            ed = doc.Editor;
            
            // valeurs par défaut initiales
            layer = (string)AcAp.GetSystemVariable("clayer");
            radius = 10.0;
        }
        
        /// <summary>
        /// Commande d'affichage de la boite de dialogue
        /// </summary>
        [CommandMethod("CMD_MODAL_WPF")]
        public void ModalWpfDialogCmd()
        {
            var layers = GetLayerNames();
            if (!layers.Contains(layer))
            {
                layer = (string)AcAp.GetSystemVariable("clayer");
            }
            // affichage de la boite de dialogue
            var dialog = new ModalWpfDialog(layers, layer, radius);
            var result = AcAp.ShowModalWindow(dialog);
            if (result.Value)
            {
                // mise à jour des champs
                layer = dialog.Layer;
                radius = dialog.Radius;
                
                // dessin du cercle
                var ppr = ed.GetPoint("\nSpécifiez le centre: ");
                if (ppr.Status == PromptStatus.OK)
                {
                    // dessin du cercle dans l'espace courant
                    using (var tr = db.TransactionManager.StartTransaction())
                    {
                        var curSpace = 
                            (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                        using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                        {
                            circle.TransformBy(ed.CurrentUserCoordinateSystem);
                            circle.Layer = layer;
                            curSpace.AppendEntity(circle);
                            tr.AddNewlyCreatedDBObject(circle, true);
                        }
                        tr.Commit();
                    }
                }
            }
        }
        
        /// <summary>
        /// Collecte les noms des calques du dessin.
        /// </summary>
        /// <returns>Collection de données des calques.</returns>
        private List<string> GetLayerNames()
        {
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                return ((LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead))
                    .Cast<ObjectId>()
                    .Select(id => ((LayerTableRecord)tr.GetObject(id, OpenMode.ForRead)).Name)
                    .ToList();
            }
        }
    }
}
            
ModalWpfDialog.xaml
<Window x:Class="AcadUISample.ModalWpf.ModalWpfDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        xmlns:local="clr-namespace:AcadUISample.ModalWpf"
        mc:Ignorable="d" 
        Title="ModalWpfDialog" 
        Height="160" Width="300" 
        MinHeight="160" MinWidth="250"
        WindowStyle="ToolWindow"
        WindowStartupLocation="CenterOwner" >
        
    <Grid Background="WhiteSmoke">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <!--Première rangée-->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5,15,5,5">Calque :</Label>
            <ComboBox x:Name="cbxLayer" Grid.Column ="1" 
                      Margin="5,15,10,5" HorizontalAlignment="Stretch" />
        </Grid>
        
        <!--Deuxième rangée-->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5">Rayon :</Label>
            <TextBox x:Name="txtRadius" Grid.Column="1" 
                     Margin="5" HorizontalAlignment="Stretch" 
                TextChanged="txtRadius_TextChanged" />
            <Button Grid.Column="2" Margin="5,5,10,5" Content="    >    " 
                    Click="btnRadius_Click" />
        </Grid>
        
        <!--Troisième rangée-->
        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Button x:Name="btnOK" Margin="10" HorizontalAlignment="Right" Content="OK" 
                    Height="24" Width="80" Click="btnOK_Click"/>
            <Button Grid.Column="1" Margin="10" HorizontalAlignment="Left" Content="Annuler" 
                    Height="24" Width="80" IsCancel="True" />
        </Grid>
    </Grid>
</Window>
            
ModalWpfDialog.xaml.cs
using Autodesk.AutoCAD.EditorInput;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;
namespace AcadUISample.ModalWpf
{
    /// <summary>
    /// Logique d'interaction pour ModalWpfDialog.xaml
    /// </summary>
    public partial class ModalWpfDialog : Window
    {
        // champ privé
        double radius;
        
        /// <summary>
        /// Obtient le nom du calque sélectionné.
        /// </summary>
        public string Layer => (string)cbxLayer.SelectedItem;
        
        /// <summary>
        /// Obtient le rayon.
        /// </summary>
        public double Radius => radius;
        
        /// <summary>
        /// Crée une nouvelle instance de ModalWpfDialog.
        /// </summary>
        /// <param name="layers">Collection des noms de calque.</param>
        /// <param name="layer">Nom du calque par défaut.</param>
        /// <param name="radius">Rayon par défaut</param>
        public ModalWpfDialog(List<string> layers, string layer, double radius)
        {
            InitializeComponent();
            this.radius = radius;
            cbxLayer.ItemsSource = layers;
            cbxLayer.SelectedItem = layer;
            txtRadius.Text = radius.ToString();
        }
        
        /// <summary>
        /// Gère l'événement 'Click' du bouton 'Rayon'.
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void btnRadius_Click(object sender, RoutedEventArgs e)
        {
            // inviter l'utilisateur à spécifier une distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
            {
                txtRadius.Text = pdr.Value.ToString();
            }
        }
        
        /// <summary>
        /// Gère l'événement 'Click' du bouton 'OK'.
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void btnOK_Click(object sender, RoutedEventArgs e)
        {
            DialogResult = true;
        }
        
        /// <summary>
        /// Gère l'événement 'TextChanged' de la zone d'édition de texte 'Rayon'.
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void txtRadius_TextChanged(object sender, TextChangedEventArgs e)
        {
            btnOK.IsEnabled = double.TryParse(txtRadius.Text, out radius);
        }
    }
}
            

Retour au sommaire.




Boite de dialogue modale (Binding)

À côté de la richesse des contenus, le WPF offre aussi un puissant mécanisme de liaison des données basé en partie sur les propriétés de dépendance (Dependency Properties), propriétés qui peuvent notifier que leur valeur a changé.

Pour pouvoir utiliser ce mécanisme, il faut affecter le contexte de données (DataContext) à une classe (ici celle contenant le code behind) et celle-ci doit implémenter l'interface INotifyPropertyChanged.

Pour illustrer ça, reprenons notre boite de dialogue modale.
Dans le XAML, on lie certaines propriétés de dépendance des contrôles à des propriétés définies dans le code behind. La classe contenant le code behind doit donc implémenter INotifyPropertyChanged.
De même, les boutons 'Rayon' et 'OK' sont liés à des instances de RoutedCommand définies dans le code behind qui gèrent leur comportement.
L'utilisation du 'Binding' en WPF permet de ne pas avoir à nommer les différents contrôles.

Pour pousser un peu les choses, ajoutons aux éléments de la liste déroulante une pastille de la couleur du calque.
Dans le XAML, à l'intérieur de la balise ComboBox, on définit un modèle d'élément (ItemTemplate) pour afficher la pastille de couleur à côté du nom du calque. Il faut donc que les éléments de la collection liée au contrôle ComboBox aient une propriété correspondant au nom du calque et une autre de type SolidColorBrush (le type utilisé en WPF pour dessiner un aplat de couleur).
On utilise ici un dictionnaire contenant les noms des calques (clé) et le pinceau correspondant à la couleur de chaque calque (valeur).

Commands.cs
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.ModalWpfBinding.Commands))]

namespace AcadUISample.ModalWpfBinding
{
    public class Commands
    {
        // champs d'instance
        Document doc;
        Database db;
        Editor ed;
        double radius;     // valeur par défaut pour le rayon
        string layerName;  // valeur par défaut pour le calque
        
        /// <summary>
        /// Crée une instance de Commands
        /// Ce constructeur est appelé une fois par document 
        /// au premier appel d'une méthode 'CommandMethod'.
        /// </summary>
        public Commands()
        {
            // initialisation des champs privés
            doc = AcAp.DocumentManager.MdiActiveDocument;
            db = doc.Database;
            ed = doc.Editor;
            
            // valeurs par défaut initiales
            layerName = (string)AcAp.GetSystemVariable("clayer");
            radius = 10.0;
        }
        
        /// <summary>
        /// Commande d'affichage de la boite de dialogue
        /// </summary>
        [CommandMethod("CMD_MODAL_WPF_BINDING")]
        public void ModalWpfDialogCmd()
        {
            var layers = GetLayerBrushes();
            if (!layers.ContainsKey(layerName))
            {
                layerName = (string)AcAp.GetSystemVariable("clayer");
            }
            var layer = layers.First(l => l.Key == layerName);
            
            // affichage de la boite de dialogue
            var dialog = new ModalWpfDialog(layers, layer, radius);
            var result = AcAp.ShowModalWindow(dialog);
            if (result.Value)
            {
                // mise à jour des champs
                layerName = dialog.Layer.Key;
                radius = dialog.Radius;
                
                // dessin du cercle
                var ppr = ed.GetPoint("\nSpécifiez le centre: ");
                if (ppr.Status == PromptStatus.OK)
                {
                    // dessin du cercle dans l'espace courant
                    using (var tr = db.TransactionManager.StartTransaction())
                    {
                        var curSpace = 
                            (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                        using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                        {
                            circle.TransformBy(ed.CurrentUserCoordinateSystem);
                            circle.Layer = layerName;
                            curSpace.AppendEntity(circle);
                            tr.AddNewlyCreatedDBObject(circle, true);
                        }
                        tr.Commit();
                    }
                }
            }
        }
        
        /// <summary>
        /// Collecte les calques du dessin et le pinceau correspondant à leur couleur.
        /// </summary>
        /// <returns>Dictionnaire des calques et de leurs pinceaux.</returns>
        private Dictionary<string, SolidColorBrush> GetLayerBrushes()
        {
            var layerBrushes = new Dictionary<string, SolidColorBrush>();
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                var layerTable = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
                foreach (ObjectId id in layerTable)
                {
                    var layer = (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead);
                    var drawingColor = layer.Color.ColorValue;
                    var mediaColor = 
                        Color.FromRgb(drawingColor.R, drawingColor.G, drawingColor.B);
                    layerBrushes.Add(layer.Name, new SolidColorBrush(mediaColor));
                }
            }
            return layerBrushes;
        }
    }
}
            
ModalWpfDialog.xaml
<Window x:Class="AcadUISample.ModalWpfBinding.ModalWpfDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AcadUISample.ModalWpfBinding"
        mc:Ignorable="d" 
        Title="ModalWpfDialog" 
        Height="160" Width="300" 
        MinHeight="160" MinWidth="250"
        WindowStyle="ToolWindow"
        WindowStartupLocation="CenterOwner" >

    <Window.CommandBindings>
        <CommandBinding Command="{x:Static local:ModalWpfDialog.OkCmd}"
                        Executed="OkCmdExecuted"
                        CanExecute="OkCmdCanExecute" />
        <CommandBinding Command="{x:Static local:ModalWpfDialog.RadiusCmd}"
                        Executed="RadiusCmdExecuted"
                        CanExecute="RadiusCanExecute" />
    </Window.CommandBindings>

    <Grid Background="WhiteSmoke">
        <Grid.RowDefinitions> 
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!--Première rangée-->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5,15,5,5">Calque :</Label>
            <ComboBox Grid.Column ="1" Margin="5,15,10,5" HorizontalAlignment="Stretch" 
                      ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}">
                <!--Définition d'un modèle pour les éléments de la liste déroulante-->
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <!--Carré de la couleur du calque-->
                            <Rectangle Grid.Column="0" Margin="3" VerticalAlignment="Stretch" 
                                       Width="{Binding Path=ActualHeight, 
                                               RelativeSource={RelativeSource Self}}"
                                       Stroke="Black" StrokeThickness="0.5" 
                                       Fill="{Binding Value}"/>
                            <!--Nom du calque-->
                            <TextBlock Grid.Column="1" VerticalAlignment="Center" 
                                       Text="{Binding Key}" />
                        </Grid>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
        </Grid>

        <!--Deuxième rangée-->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5">Rayon :</Label>
            <TextBox Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" 
                     Text="{Binding TextRadius, UpdateSourceTrigger=PropertyChanged}" />
            <Button Grid.Column="2" Margin="5,5,10,5" Content="    >    " 
                    Command="{x:Static local:ModalWpfDialog.RadiusCmd}" />
        </Grid>

        <!--Troisième rangée-->
        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Button Margin="10" HorizontalAlignment="Right" Content="OK" Height="24" Width="80" 
                    Command="{x:Static local:ModalWpfDialog.OkCmd}"/>
            <Button Grid.Column="1" Margin="10" HorizontalAlignment="Left" Content="Annuler" 
                    Height="24" Width="80" IsCancel="True" />
        </Grid>
    </Grid>
</Window>
            
ModalWpfDialog.xaml.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

using Autodesk.AutoCAD.EditorInput;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;


namespace AcadUISample.ModalWpfBinding
{
    /// <summary>
    /// Logique d'interaction pour ModalWpfDialog.xaml
    /// </summary>
    public partial class ModalWpfDialog : Window, INotifyPropertyChanged
    {
        #region Implementation de INotifyPropertyChanged

        /// <summary>
        /// Evénement déclenché lorsqu'une propriété change.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Méthode appelée dans le 'setter' des propriétés dont on veut notifier le changement.
        /// </summary>
        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        // champs privés
        KeyValuePair<string, SolidColorBrush> layer;
        double radius;
        string txtRad;
        bool validNumber;

        /// <summary>
        /// Obtient la commande liée au bouton 'OK'
        /// </summary>
        public static RoutedCommand OkCmd = new RoutedCommand();

        /// <summary>
        /// Obtient la commande liée au bouton 'Rayon'
        /// </summary>
        public static RoutedCommand RadiusCmd = new RoutedCommand();

        /// <summary>
        /// Obtient ou définit l'instance de LayerData liée au calque sélectionné dans le contrôle ComboBox.
        /// </summary>
        public KeyValuePair<string, SolidColorBrush> Layer
        {
            get { return layer; }
            set { layer = value; OnPropertyChanged(nameof(Layer)); }
        }

        /// <summary>
        /// Obtient la collection de données de calque liée aux éléments du contrôle ComboBox.
        /// </summary>
        public Dictionary<string, SolidColorBrush> Layers { get; }

        /// <summary>
        /// Obtient le rayon
        /// </summary>
        public double Radius => radius;

        /// <summary>
        /// Obtient ou définit le texte de la boite d'édition 'Rayon'
        /// </summary>
        public string TextRadius
        {
            get { return txtRad; }
            set
            {
                txtRad = value;
                ValidNumber = double.TryParse(value, out radius) && radius > 0.0;
                OnPropertyChanged(nameof(TextRadius));
            }
        }

        /// <summary>
        /// Obtient ou définit une valeur indiquant si le texte de la boite d'édition 'Rayon' représente un nombre valide.
        /// </summary>
        public bool ValidNumber
        {
            get { return validNumber; }
            set { validNumber = value; OnPropertyChanged(nameof(ValidNumber)); }
        }

        /// <summary>
        /// Crée une nouvelle instance de ModalWpfDialog
        /// </summary>
        /// <param name="layers">Collection de données de calque à lier au contrôle ComboBox.</param>
        /// <param name="layer">Données du calque par défaut.</param>
        /// <param name="radius">Rayon par défaut.</param>
        public ModalWpfDialog(Dictionary<string, SolidColorBrush> layers, KeyValuePair<string, SolidColorBrush> layer, double radius)
        {
            InitializeComponent();
            // définit le contexte de données
            DataContext = this;
            // initialise les liaisons
            Layers = layers;
            Layer = layer;
            TextRadius = radius.ToString();
        }

        /// <summary>
        /// Définit l'action liée bouton 'OK'
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void OkCmdExecuted(object sender, ExecutedRoutedEventArgs e) =>
            DialogResult = true;

        /// <summary>
        /// Détermine si l'action liée bouton 'OK' peut être exécutée
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement</param>
        private void OkCmdCanExecute(object sender, CanExecuteRoutedEventArgs e) =>
            e.CanExecute = ValidNumber;

        /// <summary>
        /// Définit l'action liée bouton 'Rayon'
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement.</param>
        private void RadiusCmdExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            // inviter l'utilisateur à spécifier une distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                TextRadius = pdr.Value.ToString();
        }

        /// <summary>
        /// Détermine si l'action liée bouton 'Rayon' peut être exécutée
        /// </summary>
        /// <param name="sender">Source de l'événement.</param>
        /// <param name="e">Données de l'événement</param>
        private void RadiusCanExecute(object sender, CanExecuteRoutedEventArgs e) =>
            e.CanExecute = true;
    }
}
            

Retour au sommaire.




Palette avec MVVM

Le modèle de conception (Design Pattern) MVVM (Model View ViewModel) est particulièrement adapté au WPF.
Il permet de séparer la partie "métier", en général les données, (Model) de leur présentation (View) et de la logique d'interaction entre les deux (ViewModel).
L'application "stricte" de cette architecture peut paraître un peu lourde (surtout dans le cas d'un exemple aussi simple que celui-ci) mais elle incite à mieux structurer le code en optimisant l'utilisation des liaisons.


La partie Model ne contient pas, ici, de code spécifique hormis la commande de dessin du cercle. On peut considérer qu'elle est constituée par l'API AutoCAD. Par exemple, la liste déroulante des calques sera liée à AutoCAD à l'aide de l'API UIBindings (une collection"dynamique" étant nécessaire dans le cas d'un affichage non modal).

Commands.cs
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

[assembly: CommandClass(typeof(AcadUISample.PaletteWpf.Commands))]

namespace AcadUISample.PaletteWpf
{
    public class Commands
    {
        // champs statiques
        static CustomPaletteSet palette;
        
        // champs d'instance (valeurs par défaut)
        double radius = 10.0;
        string layer;
        
        /// <summary>
        /// Commande d'affichage de la palette
        /// </summary>
        [CommandMethod("CMD_PALETTE_WPF")]
        public void ShowPaletteSetWpf()
        {
            if (palette == null)
                palette = new CustomPaletteSet();
            palette.Visible = true;
        }
        
        /// <summary>
        /// Commande de dessin du cercle
        /// </summary>
        [CommandMethod("CMD_CIRCLE_WPF")]
        public void DrawCircleCmd()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            var db = doc.Database;
            var ed = doc.Editor;
            
            // choix du calque
            if (string.IsNullOrEmpty(layer))
                layer = (string)Application.GetSystemVariable("clayer");
            var strOptions = new PromptStringOptions("\nNom du calque: ");
            strOptions.DefaultValue = layer;
            strOptions.UseDefaultValue = true;
            var strResult = ed.GetString(strOptions);
            if (strResult.Status != PromptStatus.OK)
                return;
            layer = strResult.StringResult;
            
            // spécification du rayon
            var distOptions = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            distOptions.DefaultValue = radius;
            distOptions.UseDefaultValue = true;
            var distResult = ed.GetDistance(distOptions);
            if (distResult.Status != PromptStatus.OK)
                return;
            radius = distResult.Value;
            
            // spécification du centre
            var ppr = ed.GetPoint("\nSpécifiez le centre: ");
            if (ppr.Status == PromptStatus.OK)
            {
                // dessin du cercle dans l'espace courant
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, distResult.Value))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = strResult.StringResult;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }
    }
}
            


Dans la partie View on trouve la description de l'interface utilisateur répartie entre :
- le fichier CustomPaletteSet.cs qui décrit le jeu de palettes ;
- le fichier PaletteTabView.xaml qui décrit le contrôle utilisateur et définit les liaisons avec des propriétés du ViewModel ;
- le fichier PaletteTabView.xaml.cs censé contenir la logique d'interaction (Code Behind) qui est, en général, quasi vide dans le cas de l'architecture MVVM (la logique d'interaction relève de la partie ViewModel)

CustomPaletteSet.cs
using System;

using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.Windows;

namespace AcadUISample.PaletteWpf
{
    internal class CustomPaletteSet : PaletteSet
    {
        // champ statique
        static bool wasVisible;

        /// <summary>
        /// Crée une nouvelle instance de CustomPaletteSet
        /// </summary>
        public CustomPaletteSet()
            :base("Palette WPF", "CMD_PALETTE_WPF", new Guid("{42425FEE-B3FD-4776-8090-DB857E9F7A0E}"))
        {
            Style =
                PaletteSetStyles.ShowAutoHideButton |
                PaletteSetStyles.ShowCloseButton |
                PaletteSetStyles.ShowPropertiesMenu;
            MinimumSize = new System.Drawing.Size(250, 150);
            AddVisual("Cercle", new PaletteTabView());

            // masquage automatique de la palette quand aucune instance de Document
            // n'est active (no document state)
            var docs = Application.DocumentManager;
            docs.DocumentBecameCurrent += (s, e) => 
                Visible = e.Document == null ? false : wasVisible;
            docs.DocumentCreated += (s, e) =>
                Visible = wasVisible;
            docs.DocumentToBeDeactivated += (s, e) => 
                wasVisible = Visible;
            docs.DocumentToBeDestroyed += (s, e) =>
            {
                wasVisible = Visible;
                if (docs.Count == 1)
                    Visible = false;
            };
        }
    }
}
      
PaletteTabView.xaml
<UserControl x:Class="AcadUISample.PaletteWpf.PaletteTabView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:AcadUISample.PaletteWpf"
             mc:Ignorable="d" 
             d:DesignHeight="140" d:DesignWidth="250">
             
    <!--Définition des ressources (convertisseur de couleur)-->
    <UserControl.Resources>
        <local:LayerColorConverter x:Key="colorConverter"/>
    </UserControl.Resources>
    
    <Grid Background="WhiteSmoke">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <!--Première rangée-->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5,15,5,5">Calque :</Label>
            <ComboBox x:Name="cbxLayer" Grid.Column ="1" Margin="5,15,10,5" 
                      HorizontalAlignment="Stretch" 
                      ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}">
                      
                <!--Définition d'un modèle pour les éléments de la liste déroulante-->
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <!--Carré de la couleur du calque-->
                            <Rectangle Grid.Column="0" Margin="3" VerticalAlignment="Stretch" 
                                       Width="{Binding Path=ActualHeight, 
                                               RelativeSource={RelativeSource Self}}"
                                       Stroke="Black" StrokeThickness="0.5" 
                                       Fill="{Binding Color,  
                                              Converter={StaticResource colorConverter}}"/>
                            <!--Nom du calque-->
                            <TextBlock Grid.Column="1" VerticalAlignment="Center" 
                                       Text="{Binding Name}" />
                        </Grid>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
        </Grid>
        
        <!--Deuxième rangée-->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5">Rayon :</Label>
            <TextBox Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" 
                     Text="{Binding TextRadius, UpdateSourceTrigger=PropertyChanged}" />
            <Button Grid.Column="2" Margin="5,5,10,5" Content="    >    " 
                    Command="{Binding GetRadiusCommand}"/>
        </Grid>
        
        <!--Troisième rangée-->
        <Button Grid.Row="2" Margin="5,15,5,5" Content="OK" Height="24" Width="80" 
                Command="{Binding DrawCircleCommand}"/>
    </Grid>
</UserControl>
            
PaletteTabView.xaml.cs
using System.Windows.Controls;
namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Logique d'interaction pour PaletteTabView.xaml
    /// </summary>
    public partial class PaletteTabView : UserControl
    {
        public PaletteTabView()
        {
            InitializeComponent();
            
            // définit la liaison de données avec la partie ViewModel
            DataContext = new PaletteTabViewModel();
        }
    }
}
            


La partie ViewModel prend en charge la logique d'interaction (ce que faisait le code behind dans l'exemple précédent).
La classe PaletteTabViewModel implémente INotifyPropertyChanged via une relation d'héritage pour gérer les propriétés comme dans l'exmple précédent.
Pour les boutons, on utilise la propriété Command à la place de l'événement 'Click' et de son gestionnaire dans le code behind, ce qui permet de séparer la présentation (View) de sa logique (ViewModel). Cette propriété doit être d'un type qui implémente l'interface ICommand.

On trouve donc dans cette partie deux petites classes assez incontournables dans l'architecture MVVM :
- ObservableObject qui implémente l'interface INotifyPropertyChanged
- RelayCommand qui implémente l'interface ICommand

On trouve aussi, dans la partie ViewModel, certaines ressources nécessaires à la présentation, avec une classe LayerColorConverter pour convertir un élément de type Autodesk.AutoCAD.Colors.Color représentant la couleur d'un calque (éléments de la liste déroulante) en une instance de SolidColorBrush de la couleur correspondante ; le remplissage de la pastille sera lié à ce Converter.

PaletteTabViewModel.cs
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Windows.Data;
using System.ComponentModel;
using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.PaletteWpf
{
    class PaletteTabViewModel : ObservableObject
    {
        // champs privés
        ICustomTypeDescriptor layer;
        double radius;
        string txtRad;
        bool validRad;
        
        /// <summary>
        /// Obtient l'objet Command lié au bouton OK.
        /// Le bouton est automatiquement grisé si le prédicat CanExecute retourne false.
        /// </summary>
        public RelayCommand DrawCircleCommand =>
            new RelayCommand((_) => DrawCircle(), (_) => validRad);
            
        /// <summary>
        /// Obtient l'objet Command lié au bouton Rayon (>).
        /// </summary>
        public RelayCommand GetRadiusCommand =>
            new RelayCommand((_) => GetRadius(), (_) => true);
            
        /// <summary>
        /// Obtient ou définit le calque sélectionné.
        /// </summary>
        public ICustomTypeDescriptor Layer
        {
            get { return layer; }
            set { layer = value; OnPropertyChanged(nameof(Layer)); }
        }
        
        /// <summary>
        /// Obtient la collection des calques.
        /// </summary>
        public DataItemCollection Layers => AcAp.UIBindings.Collections.Layers;
        
        /// <summary>
        /// Obtient ou définit la valeur du rayon apparaissant dans la boite de texte.
        /// </summary>
        public string TextRadius
        {
            get { return txtRad; }
            set
            {
                txtRad = value;
                validRad = double.TryParse(value, out radius) && radius > 0.0;
                OnPropertyChanged(nameof(TextRadius));
            }
        }
        
        /// <summary>
        /// Crée une nouvelle instance de PaletteTabViewModel.
        /// </summary>
        public PaletteTabViewModel()
        {
            TextRadius = "10";
            Layer = Layers.CurrentItem;
            Layers.CollectionChanged += (s, e) => Layer = Layers.CurrentItem;
        }
        
        /// <summary>
        /// Méthode appelée par DrawCircleCommand. 
        /// Appelle la commande CMD_CIRCLE_WPF avec les options courantes
        /// </summary>
        private void DrawCircle() =>
            AcAp.DocumentManager.MdiActiveDocument?.SendStringToExecute(
                $"CMD_CIRCLE_WPF \"{((INamedValue)Layer).Name}\" {TextRadius} ", 
                false, false, false);
                
        /// <summary>
        /// Méthode appelée par GetRadiusCommand.
        /// </summary>
        private void GetRadius()
        {
            // inviter l'utilisateur à spécifier une distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpécifiez le rayon: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                TextRadius = pdr.Value.ToString();
        }
    }
}
            
ObservableObject.cs
using System.ComponentModel;

namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Fournit un type qui implémente INotifyPropertyChanged
    /// </summary>
    class ObservableObject : INotifyPropertyChanged
    {
        /// <summary>
        /// Evénement déclenché lorsqu'une propriété change.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        
        /// <summary>
        /// Méthode appelée dans le 'setter' des propriétés dont on veut notifier le changement.
        /// </summary>
        /// <param name="propertyName">Nom de la propriété.</param>
        protected void OnPropertyChanged(string propertyName) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
            
RelayCommand.cs
using System;
using System.Windows.Input;

namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Fournit un type qui implémente ICommand
    /// </summary>
    class RelayCommand : ICommand
    {
        readonly Action<object> execute;
        readonly Predicate<object> canExecute;
        
        /// <summary>
        /// Crée une nouvelle instance de RelayCommand.
        /// </summary>
        /// <param name="execute">Action à exécuter.</param>
        /// <param name="canExecute">Prédicat indiquent si l'action peut être exécutée.</param>
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }
        
        /// <summary>
        /// Exécute l'action passée en paramètre au constructeur.
        /// </summary>
        /// <param name="parameter">Paramètre de l'action (peut être null).</param>
        public void Execute(object parameter) => execute(parameter);
        
        /// <summary>
        /// Exécute le prédicat passé en paramètre au constructeur.
        /// </summary>
        /// <param name="parameter">Paramètre du prédicat (peut être null).</param>
        /// <returns>Résultat de l'exécution du prédicat.</returns>
        public bool CanExecute(object parameter) => canExecute(parameter);
        
        /// <summary>
        /// Evénement indiquant que la valeur de retour du prédicat a changé.
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }
}
            
LayerColorConverter.cs
using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;

namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Fournit la méthode de conversion pour obtenir la couleur du calque
    /// </summary>
    [ValueConversion(typeof(ICustomTypeDescriptor), typeof(SolidColorBrush))]
    class LayerColorConverter : IValueConverter
    {
        /// <summary>
        /// Convertit un objet Autodesk.AutoCAD.Colors.Color représentant la couleur d'un calque en une instance de System.Media.SolidColorBrush
        /// </summary>
        /// <param name="value">Couleur à convertir.</param>
        /// <param name="targetType">Type SolidColorBrush</param>
        /// <param name="parameter">Non utilisé.</param>
        /// <param name="culture">Non utilisé.</param>
        /// <returns>Instance de SolidColorBrush représentant la couleur du calque.</returns>
        public object Convert(
            object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value != null && value is Autodesk.AutoCAD.Colors.Color)
            {
                var acadColor = (Autodesk.AutoCAD.Colors.Color)value;
                var drawingColor = acadColor.ColorValue;
                var mediaColor = Color.FromRgb(drawingColor.R, drawingColor.G, drawingColor.B);
                return new SolidColorBrush(mediaColor);
            }
            return null;
        }
        
        /// <summary>
        /// Méthode de conversion inverse non utilisée.
        /// </summary>
        /// <returns>Toujours null</returns>
        public object ConvertBack(
            object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}
            

Retour au sommaire.


Gilles Chanteau

Solarized Color Scheme by Etan Schoonover