Aller au contenu

Les interfaces utilisateur


(gile)

Messages recommandés

Salut,

 

Pour essayer de mettre un peu certaines choses au clair, autant pour moi que pour ceux que ça pourrait aider, je vais tenter de faire un aperçu avec un exemple simple de différentes interfaces utilisateur utilisables avec AutoCAD .NET (C#).

Ce sujet servira peut-être de brouillon pour un éventuel futur tutoriel.

Toutes les remarques questions, suggestions sont donc les bienvenues.

 

Avant propos

On ne s'attardera pas sur l'(in)utilité de ce que réalise l'exemple, j'ai sciemment choisi une tâche simple pour qu'elle ne brouille pas trop le propos, à savoir le passage des données entre les différents composant du programme.

La tâche consiste donc 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 à chaque fois un peu de complexité.

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 le passage des données 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.

 

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;

using AcAp = Autodesk.AutoCAD.ApplicationServices.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);
                   var ucs = ed.CurrentUserCoordinateSystem;
                   var center = ppr.Value.TransformBy(ucs);
                   using (var circle = new Circle(center, ucs.CoordinateSystem3d.Zaxis, radius))
                   {
                       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;
       }
   }
}

 

(à suivre : Boite de dialogue modale)

  • Upvote 1

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

Boite de dialogue modale

 

http://gilecad.azurewebsites.net/Tutorials/AcadUserInterfaces/ModalDialog.png

 

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 modifiera celui-ci en conséquence). La liste des noms de calque sera liée comme source de données à la liste déroulante du formulaire.

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.

 

La classe Commands dans laquelle est définie la commande :

 

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;

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");
           }
           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);
                   var ucs = ed.CurrentUserCoordinateSystem;
                   using (var circle = new Circle(ppr.Value.TransformBy(ucs), ucs.CoordinateSystem3d.Zaxis, radius))
                   {
                       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;
       }
   }
}

 

La classe ModalDialog qui définit la boite de dialogue :

 

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(string[] layers, string clayer)
       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();
       }
   }
}

 

(à suivre : Boite de dialogue non modale)

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

Boite de dialogue non-modale

 

http://gilecad.azurewebsites.net/Tutorials/AcadUserInterfaces/ModelessDialog.png

 

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 de gérer les éventuels changements de données et/ou 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, notamment certaines collections comme celle des calques, avec les interfaces utilisateurs.

Les collections DataItemCollection obtenues avec UIBindings implémentent plusieurs interfaces dont INotitfyCollectionChanged qui, comme son nom l'indique, notifie les 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 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.

 

 

La classe Commands qui affiche la boite de dialogue :

 

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);
       }
   }
}

 

La classe ModelessDialog qui définit la boite de dialogue (et toutes les interactions avec AutoCAD) :

 

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Windows.Data;
using System;
using System.Linq;
using System.Windows.Forms;

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);
                   var ucs = ed.CurrentUserCoordinateSystem;
                   using (var circle = new Circle(ppr.Value.TransformBy(ucs), ucs.CoordinateSystem3d.Zaxis, radius))
                   {
                       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;
       }
   }
}

 

(à suivre : Palette)

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

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 est surtout là pour illustrer la gestion d'un formulaire non modal.

 

http://gilecad.azurewebsites.net/Tutorials/AcadUserInterfaces/Palette.png

 

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.

 

Chaque onglet de la palette est une instance de 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 le premier message 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 passer le focus et de verrouiller le document actif.

En prime, on peut rappeler la commande par Entrée et valider les valeurs par défaut de même.

 

 

La classe Commands qui définit la commande pour afficher la palette et la commande de dessin du cercle en ligne de commande :

 

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using System;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

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

namespace AcadUISample.Palette
{
   public class Commands
   {
       // champs statiques
       static PaletteSet palette;
       static bool wasVisible;

       // 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étion de la palette au premier appel de la commande
           if (palette == null)
           {
               palette = new PaletteSet("Palette", "CMD_PALETTE", new Guid("{1836C7AC-C70E-4CF7-AA05-F6298D275046}"));
               palette.Style =
                   PaletteSetStyles.ShowAutoHideButton |
                   PaletteSetStyles.ShowCloseButton |
                   PaletteSetStyles.ShowPropertiesMenu;
               palette.MinimumSize = new System.Drawing.Size(250, 150);
               palette.Add("Cercle", new PaletteTab());

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

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

           // choix du calque
           if (string.IsNullOrEmpty(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;
           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);
                   var ucs = ed.CurrentUserCoordinateSystem;
                   using (var circle = new Circle(ppr.Value.TransformBy(ucs), ucs.CoordinateSystem3d.Zaxis, distResult.Value))
                   {
                       circle.Layer = strResult.StringResult;
                       curSpace.AppendEntity(circle);
                       tr.AddNewlyCreatedDBObject(circle, true);
                   }
                   tr.Commit();
               }
           }
       }
   }
}

 

La classe PaletteTab qui définit le comportement de l'unique onglet de la palette :

 

using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Windows.Data;
using System;
using System.Drawing;
using System.Windows.Forms;

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 {((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;
       }
   }
}

 

(à suivre : Et avec WPF ?...)

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

Salut (gile).

 

Avec des tutos de cette qualité, on va pouvoir se lancer dans le C#...

 

Un grand merci pour le temps passé et le partage...

 

Denis...

Windows 11 / AutoCAD 2024

Sur terre, il y a 10 types de personnes, celles qui comptent en binaire et les autres (developpez.net).
Davantage d'avantages, avantagent davantage (Bobby Lapointe).
La connaissance s'accroît quand on la partage (Socrate).
Tant va la cruche à l'eau que l'habit n'amasse pas mousse avant de l'avoir tué. (Moi)

Lien vers le commentaire
Partager sur d’autres sites

Merci à tous.

 

Et avec WPF ?...

 

Tous les exemple précédents utilisent la technologie WinForms.

WPF (Windows Presentation Fundation) est la "nouvelle technologie" (.NET 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 fichier .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 WPF

 

http://gilecad.azurewebsites.net/Tutorials/AcadUserInterfaces/ModalDialogWpf.png

 

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

 

Le fichier Command.cs (semblable à celui de la boite modale WinForm) :

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using System.Linq;

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);
                       var ucs = ed.CurrentUserCoordinateSystem;
                       using (var circle = new Circle(ppr.Value.TransformBy(ucs), ucs.CoordinateSystem3d.Zaxis, radius))
                       {
                           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();
           }
       }
   }
}

 

Le fichier 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>

 

Le code behind :

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);
       }
   }
}

 

(à suivre : Boite de dialogue modale WPF 2 : Binding)

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

Boite de dialogue modale WPF 2 : Binding

 

https://gilecad.azurewebsites.net/Tutorials/AcadUserInterfaces/ModalWpfDialog.png

 

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

 

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 est une autre de type SolidColorBrush (le type utilisé en WPF pour dessiner un aplat de couleur).

On va utiliser un dictionnaire contenant les noms de calque (clé) et le pinceau correspondant à la couleur du calque.

 

Le fichier Command.cs (pas de changement si ce n'est le dictionnaire à la place de la liste passé au constructeur de la boite de dialogue) :

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
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);
                       var ucs = ed.CurrentUserCoordinateSystem;
                       using (var circle = new Circle(ppr.Value.TransformBy(ucs), ucs.CoordinateSystem3d.Zaxis, radius))
                       {
                           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;
       }
   }
}
 

 

Le XAML (il n'est plus nécessaire de nommer les contrôles, le "Binding" suffit)

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

   <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="    >    " Click="btnRadius_Click" />
       </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" IsEnabled="{Binding ValidNumber}" Click="btnOK_Click"/>
           <Button Grid.Column="1" Margin="10" HorizontalAlignment="Left" Content="Annuler" Height="24" Width="80" IsCancel="True" />
       </Grid>
   </Grid>
</Window>
 

 

Le code behind les propriétés liées à des propriétés de dépendance des contrôles doivent notifier à l'interface qu'elles ont été modifiées.

using Autodesk.AutoCAD.EditorInput;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
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 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>
       /// 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 '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)
               TextRadius = pdr.Value.ToString();
       }
   }
}
 

 

(à suivre : Palette et MVVM)

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

Palette et MVVM

 

https://gilecad.azurewebsites.net/Tutorials/AcadUserInterfaces/PaletteWPF.png

 

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

 

La classe Commands semblable à celle de l'exemple de palette avec WinForm (seule la méthode pour ajouter un contrôle utilisateur WPF diffère).

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using System;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

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

namespace AcadUISample.PaletteWpf
{
   public class Commands
   {
       // champs statiques
       static PaletteSet palette;
       static bool wasVisible;

       // 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 PaletteSet("Palette WPF", "CMD_PALETTE_WPF", new Guid("{42425FEE-B3FD-4776-8090-DB857E9F7A0E}"));
               palette.Style =
                   PaletteSetStyles.ShowAutoHideButton |
                   PaletteSetStyles.ShowCloseButton |
                   PaletteSetStyles.ShowPropertiesMenu;
               palette.MinimumSize = new System.Drawing.Size(250, 150);
               palette.AddVisual("Cercle", new PaletteTabView());

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

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

           // choix du calque
           if (string.IsNullOrEmpty(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;
           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);
                   var ucs = ed.CurrentUserCoordinateSystem;
                   using (var circle = new Circle(ppr.Value.TransformBy(ucs), ucs.CoordinateSystem3d.Zaxis, distResult.Value))
                   {
                       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 PaletteTabView.xaml qui décrit le contrôle utilisateur et définit les liaisons avec des propriétés du ViewModel ;
  • la fichier PaletteTabView.xaml.cs sensé contenir la logique d'interaction (Code Behind) est, en général, quasi vide dans le cas de l'architecture MVVM (la logique dinteraction relève de la partie ViewModel). Ici, on a fait une petite entorse pour mettre à jour les pastilles de couleur (la couleur n'étant pas une propriété de dépendance des objets DataItem).

 

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 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
           var vm = new PaletteTabViewModel();
           DataContext = vm;

           // nécessaire pour la mise à jour de la pastille de couleur si la couleur d'un calque est modifiée
           vm.Layers.ItemsChanged += (s, e) =>
           {
               int i = cbxLayer.SelectedIndex;
               cbxLayer.SelectedIndex = -1;
               cbxLayer.Items.Refresh();
               cbxLayer.SelectedIndex = i;
           };
       }
   }
}
 

 

 

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 View, certaines ressources nécessaires à la présentation, avec une classe LayerColorConverter pour convertir un élément de DataItemCollection représentant 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 ICustomTypeDescriptor représentant un calque en sa couleur
       /// </summary>
       /// <param name="value">Objet implémentant ICustomTypeDescriptor à 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 ICustomTypeDescriptor)
           {
               var item = (ICustomTypeDescriptor)value;
               var acadColor = (Autodesk.AutoCAD.Colors.Color)item.GetProperties()["Color"].GetValue(item);
               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;
       }
   }
}
 

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

  • 2 semaines après...

J'ai trouvé le moyen de faire fonctionner le binding pour les carrés de couleur sans utiliser de code behind :

 

Modif dans PaletteTabView.xaml :

 

<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}}"/>

 

Et le LayerColorConverter modifié :

 


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(Autodesk.AutoCAD.Colors.Color), typeof(SolidColorBrush))]
class LayerColorConverter : IValueConverter
{
/// <summary>
/// Convertit une couleur Autodesk.AutoCAD.Colors.Color en SolidColorBrush
/// </summary>
/// <param name="value">Couleur Autodesk.AutoCAD.Colors.Color à 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;
}
}
}
[/Code]

 

Du coup moins de code behind dans PaletteTabView.xaml.cs :

 

[Code]
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
var vm = new PaletteTabViewModel();
DataContext = vm;

}
}
}
[/Code]

 

;)

Lien vers le commentaire
Partager sur d’autres sites

Oui le binding est vraiment très puissant !

 

Pour terminer on peut aussi supprimer le code behind restant (Instanciation ViewModel + Affectation DataContext) en le mettant dans le 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">
   <UserControl.DataContext>
       <local:PaletteTabViewModel />
   </UserControl.DataContext>
...

 

Du coup, plus de code behind ! :

 

using System.Windows.Controls;

namespace AcadUISample.PaletteWpf
{
   /// <summary>
   /// Logique d'interaction pour PaletteTabView.xaml
   /// </summary>
   public partial class PaletteTabView : UserControl
   {
       public PaletteTabView()
       {
           InitializeComponent();
       }
   }
}

 

Je suis aussi tombé sur un petit bug lorsqu'on a des calques dont les noms contiennent un espace, il faut rajouter des guillemets à ta méthode DrawCircle :

 

private void DrawCircle() =>
    AcAp.DocumentManager.MdiActiveDocument?.SendStringToExecute(
    $"CMD_CIRCLE_WPF \"{((INamedValue)Layer).Name}\" {TextRadius} ", false, false, false);

 

A+,

 

Manu

Lien vers le commentaire
Partager sur d’autres sites

pou lala... je suis loin de comprendre, je me contente d’admirer et de remercier pour ceux qui y trouveront une utilité...

 

On est loin de la mode du "low code" voire du "no code" dont ou voit le bout du nez se pointer avec Dynamo dans Revit.

bon, c'est pas fait pour la même utilisation... mais quand je voie les efforts à déployer pour une pauvre boite de dialogue, je pleure...

Lien vers le commentaire
Partager sur d’autres sites

Pour terminer on peut aussi supprimer le code behind restant (Instanciation ViewModel + Affectation DataContext) en le mettant dans le Xaml

 

Oui je n'ai pas précisé ça ici parce que je ne pense pas que ce soit une véritable entorse au MVVM (même chez les puristes/intégristes), certains préfèrent spécifier l'affectation du DataContext dans le constructeur de la classe :

        public PaletteTabView()
       {
           InitializeComponent();
           // définit la liaison de données avec la partie ViewModel
           DataContext = new PaletteTabViewModel();
       }

 

Je suis aussi tombé sur un petit bug lorsqu'on a des calques dont les noms contiennent un espace, il faut rajouter des guillemets à ta méthode DrawCircle :

 

private void DrawCircle() =>
    AcAp.DocumentManager.MdiActiveDocument?.SendStringToExecute(
    $"CMD_CIRCLE_WPF \"{((INamedValue)Layer).Name}\" {TextRadius} ", false, false, false);

 

Merci de le signaler. je suis tellement partisan de ne jamais utiliser ni espace ni autre caractère non alphanumérique excepté l'underscore dans les noms de calques, de blocs, de fichier, etc. que j'ai parfois tendance à oublier qu'il puisse en être autrement.

 

Merci encore.

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

pou lala... je suis loin de comprendre, je me contente d’admirer et de remercier pour ceux qui y trouveront une utilité...

 

On est loin de la mode du "low code" voire du "no code" dont ou voit le bout du nez se pointer avec Dynamo dans Revit.

bon, c'est pas fait pour la même utilisation... mais quand je voie les efforts à déployer pour une pauvre boite de dialogue, je pleure...

 

Ce sujet a la prétention d'être une forme de tutoriel avec des exemples de code, donc, forcément, il y en a, du code...

 

Si .NET est considéré comme un environnement de "haut niveau" par rapport à C/C++, c'est quand même beaucoup plus bas niveau que Visual LISP ou VBA (plus de puissance => plus de contrôle => plus de responsabilités => plus de code).

 

Si on parle de "simple boite de dialogue", il faut voir les réponse #2 et #12 (éventuellement #13) et si on supprime les commentaires, du code, il n'y en a pas tant.

 

Mais si on considère les affichage non modaux (boite non modale, palette), on peut moins parler de simplicité surtout quand l'interface affiche des données non statiques comme la liste des calques.

Gilles Chanteau - gileCAD - GitHub
Développements sur mesure pour AutoCAD

Lien vers le commentaire
Partager sur d’autres sites

Oui je n'ai pas précisé ça ici parce que je ne pense pas que ce soit une véritable entorse au MVVM (même chez les puristes/intégristes), certains préfèrent spécifier l'affectation du DataContext dans le constructeur de la classe :

        public PaletteTabView()
       {
           InitializeComponent();
           // définit la liaison de données avec la partie ViewModel
           DataContext = new PaletteTabViewModel();
       }

 

Je me doutais de ta réponse vu tes compétences, mais c'était au cas où, ça peut peut-être servir à d'autres...

Tout à fait d'accord sur le fait que ce ne soit pas une entorse au MVVM, c'était juste pour le plaisir de ne rien laisser dans le code behind

Lien vers le commentaire
Partager sur d’autres sites

  • 3 semaines après...

Salut,

 

Le corps de ce sujet remis en forme est en ligne sur gileCAD (dans la rubrique tutoriels).

 

Bonjour Gile,

 

Pour info, deux petites coquilles suite au changement du binding de la couleur :

 

Le LayerColorConverter ne se trouve plus dans le viewmodel :

 

On trouve aussi, dans la partie ViewModel, certaines ressources nécessaires à la présentation, avec une classe LayerColorConverter pour convertir un élément de DataItemCollection représentant 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.

 

Le converter ne convertit plus un calque en Brush, mais un Autodesk.AutoCAD.Colors.Color en Brush :

 

/// <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 ICustomTypeDescriptor représentant un calque en sa couleur

/// </summary>

/// <param name="value">Objet implémentant ICustomTypeDescriptor à 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>

Lien vers le commentaire
Partager sur d’autres sites

Créer un compte ou se connecter pour commenter

Vous devez être membre afin de pouvoir déposer un commentaire

Créer un compte

Créez un compte sur notre communauté. C’est facile !

Créer un nouveau compte

Se connecter

Vous avez déjà un compte ? Connectez-vous ici.

Connectez-vous maintenant
×
×
  • Créer...

Information importante

Nous avons placé des cookies sur votre appareil pour aider à améliorer ce site. Vous pouvez choisir d’ajuster vos paramètres de cookie, sinon nous supposerons que vous êtes d’accord pour continuer. Politique de confidentialité