« May 2007 | Main | July 2007 »

Using a modeless .NET dialog to display AutoCAD object properties

In this previous post we looked at creating a simple modal dialog and using it to display object properties. This post looks at the structural changes you need to make to your application for the same dialog to be used modelessly. In a later post we'll look at the benefits you get from leveraging the Palette system for modeless interaction inside AutoCAD.

Firstly, let's think about the interaction paradigm needed by a modeless dialog. A few things come to mind:

  • There is no longer a need to hide and show the dialog around selection
  • Rather than asking the user to select an entity, it's neater to respond to standard selection events
  • We no longer need a "browse" button
  • We now need to be more careful about document access
    • Our command automatically locked the document (and had sole access) in the modal example
    • We should now lock it "manually" when we access it

So we can already simplify our dialog class - here's some modified C# code, with the browse button removed and document-locking in place:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;


namespace CustomDialogs

{

  public partial class TypeViewerForm : Form

  {

    public TypeViewerForm()

    {

      InitializeComponent();

    }


    public void SetObjectText(string text)

    {

      typeTextBox.Text = text;

    }


    public void SetObjectId(ObjectId id)

    {

      if (id == ObjectId.Null)

      {

        SetObjectText("");

      }

      else

      {

        Document doc =

          Autodesk.AutoCAD.ApplicationServices.

            Application.DocumentManager.MdiActiveDocument;

        DocumentLock loc =

          doc.LockDocument();

        using (loc)

        {

          Transaction tr =

            doc.TransactionManager.StartTransaction();

          using (tr)

          {

            DBObject obj = tr.GetObject(id, OpenMode.ForRead);

            SetObjectText(obj.GetType().ToString());

            tr.Commit();

          }

        }

      }

    }

    private void closeButton_Click(object sender, EventArgs e)

    {

      this.Close();

    }

  }

}

So which event should we respond to, to find out when objects are selected? In this case I chose a PointMonitor - this class tells you a lot of really useful information about the current selection process. It also has the advantage of picking up the act of hovering over objects - no need for selection to actually happen. One other fun option would have been to use a Database event (ObjectAppended) to display information about objects as they are added to the drawing.

A few other comments about the code:

  • Predictably enough we now use ShowModelessDialog rather than ShowModalDialog()
  • We have our form as a member variable of the class, as its lifespan goes beyond the command we use to show it
  • I've also removed the selection code; we're no longer asking for objects to be selected

Here's the updated command implementation:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using CustomDialogs;


namespace CustomDialogs

{

  public class Commands

  {

    TypeViewerForm tvf;


    public Commands()

    {

      tvf = new TypeViewerForm();

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      ed.PointMonitor +=

        new PointMonitorEventHandler(OnMonitorPoint);

    }


    ~Commands()

    {

      try

      {

        tvf.Dispose();

        Document doc =

          Application.DocumentManager.MdiActiveDocument;

        Editor ed = doc.Editor;

        ed.PointMonitor -=

          new PointMonitorEventHandler(OnMonitorPoint);

      }

      catch(System.Exception)

      {

        // The editor may no longer

        // be available on unload

      }

    }


    private void OnMonitorPoint(

      object sender,

      PointMonitorEventArgs e

    )

    {

      FullSubentityPath[] paths =

        e.Context.GetPickedEntities();

      if (paths.Length <= 0)

      {

        tvf.SetObjectId(ObjectId.Null);

        return;

      };


      ObjectId[] objs = paths[0].GetObjectIds();

      if (objs.Length <= 0)

      {

        tvf.SetObjectId(ObjectId.Null);

        return;

      };


      // Set the "selected" object to be the last in the list

      tvf.SetObjectId(objs[objs.Length - 1]);

    }


    [CommandMethod("vt",CommandFlags.UsePickSet)]

    public void ViewType()

    {

      Application.ShowModelessDialog(null, tvf, false);

    }

  }

}

And here's the source project for this version of the application. When you run the application you may experience issues with the dialog getting/retaining focus - this is generally a problem with modeless dialogs that has been addressed automatically by the Palette class, something we'll take a look at in a future post.

June 29, 2007 in AutoCAD, AutoCAD .NET, Object properties, User interface | Permalink | Comments (9) | TrackBack

Using a modal .NET dialog to display AutoCAD object properties

Firstly, a big thanks for all your comments on the first anniversary post. It's good to know that people are finding this blog useful, and I hope the flow of ideas (internal and external) doesn't dry up anytime soon. So keep the comments coming! :-)

This post is going to start a sequence of posts that look at how to integrate forms into AutoCAD. This post looks at modal forms, and later on we'll look more at modeless forms and - in particular - palettes.

Just to be clear, these posts will focus on the basic integration - the more advanced activity of detailed property display (etc.) are left as an exercise for the reader. For example, in today's code we're simply going to get the type of an object and put that text into our dialog. Nothing very complex, but it shows the basic interaction between AutoCAD objects and WinForms.

Before we get started, I should very quickly define "modal" and "modeless", for those that aren't familiar with the terminology. Modal dialogs take exclusive control of an application's user-input functions, while modeless dialogs can co-exist with other modeless dialogs and user-input handling. Which means that the two types of dialog have different issues to deal with (in terms of how the assumptions they make on accessing data etc.).

So next we need to create our form... I'm not going to step through the process here - we'll focus on the code - so to make life easier I've packaged up the source here.

Here's the C# code for the command class. It's very simple: it checks the pickfirst selection and uses the first object as input for the form (assigning the object ID to the form, which will then go away and retrieve the object's type). At this stage we're just supporting one object - we're not going through the effort of determining shared properties across objects etc. We then use Application.ShowModalDialog() to show the form inside AutoCAD.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using CustomDialogs;


namespace CustomDialogs

{

  public class Commands

  {

    [CommandMethod("vt",CommandFlags.UsePickSet)]

    public void ViewType()

    {

      Editor ed =

        Application.DocumentManager.MdiActiveDocument.Editor;


      TypeViewerForm tvf = new TypeViewerForm();

      PromptSelectionResult psr =

        ed.GetSelection();

      if (psr.Value.Count > 0)

      {

        ObjectId selId = psr.Value[0].ObjectId;

        tvf.SetObjectId(selId);

      }

      if (psr.Value.Count > 1)

      {

        ed.WriteMessage(

          "\nMore than one object was selected: only using the first.\n"

        );

      }

      Application.ShowModalDialog(null, tvf, false);

    }

  }

}

The form itself is a little more complex (but barely). It contains a function that can be used to set the active object (by its ID, as mentioned above) which goes away and opens the object, getting its type. The form also contains a "browse" button, which can be used to change the actively selected object.

Here's the C# code for the form:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;


namespace CustomDialogs

{

  public partial class TypeViewerForm : Form

  {

    public TypeViewerForm()

    {

      InitializeComponent();

    }


    public void SetObjectText(string text)

    {

      typeTextBox.Text = text;

    }


    public void SetObjectId(ObjectId id)

    {

      if (id == ObjectId.Null)

      {

        SetObjectText("");

      }

      else

      {

        Document doc =

          Autodesk.AutoCAD.ApplicationServices.

            Application.DocumentManager.MdiActiveDocument;

        Transaction tr =

          doc.TransactionManager.StartTransaction();

        using (tr)

        {

          DBObject obj = tr.GetObject(id, OpenMode.ForRead);

          SetObjectText(obj.GetType().ToString());

          tr.Commit();

        }

      }

    }

    private void closeButton_Click(object sender, EventArgs e)

    {

      this.Close();

    }


    private void browseButton_Click(object sender, EventArgs e)

    {

      DocumentCollection dm =

        Autodesk.AutoCAD.ApplicationServices.

          Application.DocumentManager;

      Editor ed =

        dm.MdiActiveDocument.Editor;


      Hide();

      PromptEntityResult per =

        ed.GetEntity("\nSelect entity: ");

      if (per.Status == PromptStatus.OK)

      {

        SetObjectId(per.ObjectId);

      }

      else

      {

        SetObjectId(ObjectId.Null);

      }

      Show();

    }

  }

}

Here's what happens when you run the VT command and select a circle:

Modal_dialog_1

As an aside... I'm going on holiday on Wednesday for a week. We have our annual football (meaning soccer, of course) tournament being held this year in the UK. Teams from Autodesk offices from around the world (it used to have a European focus, but it's gained in popularity in the last few years) will fly in and take part. I'll be playing for one of the Swiss teams, and it'll certainly be lots of fun to catch up with old friends from other locations. The tournament is at the weekend, but I'm taking some days off either side to catch up with family and friends. I doubt I'll have time to queue up additional posts for while I'm away, so this is just to let you know that things may go a little quiet for the next week or so.

June 25, 2007 in AutoCAD, AutoCAD .NET, Object properties, User interface | Permalink | Comments (2) | TrackBack

One year wiser?

Well, would you believe it: Through the Interface was started exactly a year ago today.

I hope you've found this blog helpful over the past 12 months; I've certainly enjoyed sharing my (and above all, my team's) knowledge with you, and look forward to doing so for a long time to come. But please keep the post suggestions coming, as many of the best posts over the last year (in my opinion, at least) have originated from suggestions from this blog's readership.

By the way, for those of you who are interested: I'm now back from Beijing - we arrived in Switzerland a little over 2 weeks ago, having stopped by Bangalore for 2 weeks on the way home to spend time with the DevTech team there and catch up with friends and relatives. As you can imagine, 2+ months away (7 weeks in China, 2 weeks in India) was quite tough, even if we were all together as a family. We very often missed our home comforts, it has to be said, but the experience was certainly valuable.

Two days after getting back from the trip to China & India, I had to head across to Munich for some internal meetings. This afternoon I'm heading to Boston for some more. But all being well I should have another technical post for you by the end of the week.

June 19, 2007 in Personal | Permalink | Comments (10) | TrackBack

Creating a table of block attributes in AutoCAD using .NET - Part 2

In the last post we looked at some code to create a table of attribute values for a particular block. In this post we'll extend that code and show how to use a formula to create a total of those values.

Below is the C# code. I've numbered the lines, and those in red are new since the last post. The complete source file can be downloaded here.

Firstly, a quick breakdown of the changes:

  • Lines 60-81 deal with user input, and the forcing of the decision to "embed" rather than "link", if we're performing the total (table formulae do not work with fields, even if they have numeric results, so we're forced to create the table with the current value, rather than a field pointing to the attribute reference)
  • Line 134 and subsequently lines 159-166 declare and set a variable indicating for which column we're going to provide a total
  • Lines 169-181 deal with the exceptional case that we don't find the specified attribute definition
  • Lines 310-336 create our additional row, and insert the total in the appropriate cell. We're using a formula such as this: %<\AcExpr (Sum(A2:A4)) \f "%lu2%pr2">%
    • The \f flag specifies we want a numeric value with 2 decimal places - these codes are not documented, but you can find them out by using the FIELD command, as described in this previous post
  • Line 343 performs a regen, to update the value of our field

And now for the code:

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.DatabaseServices;

    3 using Autodesk.AutoCAD.EditorInput;

    4 using Autodesk.AutoCAD.Geometry;

    5 using Autodesk.AutoCAD.Runtime;

    6 using System.Collections.Specialized;

    7 using System;

    8

    9 namespace TableCreation

   10 {

   11   public class Commands

   12   {

   13     // Set up some formatting constants

   14     // for the table

   15

   16     const double colWidth = 15.0;

   17     const double rowHeight = 3.0;

   18     const double textHeight = 1.0;

   19     const CellAlignment cellAlign =

   20       CellAlignment.MiddleCenter;

   21

   22     // Helper function to set text height

   23     // and alignment of specific cells,

   24     // as well as inserting the text

   25

   26     static public void SetCellText(

   27       Table tb,

   28       int row,

   29       int col,

   30       string value

   31     )

   32     {

   33       tb.SetAlignment(row, col, cellAlign);

   34       tb.SetTextHeight(row, col, textHeight);

   35       tb.SetTextString(row, col, value);

   36     }

   37

   38     [CommandMethod("BAT")]

   39     static public void BlockAttributeTable()

   40     {

   41       Document doc =

   42         Application.DocumentManager.MdiActiveDocument;

   43       Database db = doc.Database;

   44       Editor ed = doc.Editor;

   45

   46       // Ask for the name of the block to find

   47

   48       PromptStringOptions opt =

   49         new PromptStringOptions(

   50           "\nEnter name of block to list: "

   51         );

   52       PromptResult pr = ed.GetString(opt);

   53

   54       if (pr.Status == PromptStatus.OK)

   55       {

   56         string blockToFind =

   57           pr.StringResult.ToUpper();

   58         bool embed = false;

   59

   60         // And the attribute to provide total for

   61

   62         opt.Message =

   63           "\nEnter name of column to total <\"\">: ";

   64         pr = ed.GetString(opt);

   65

   66         if (pr.Status == PromptStatus.None ||

   67             pr.Status == PromptStatus.OK)

   68         {

   69           string columnToTotal =

   70               pr.StringResult.ToUpper();

   71

   72           if (columnToTotal != "")

   73           {

   74             // If a column has been chosen, we need

   75             // to embed the attribute values

   76             // as otherwise the "sum" formula will fail

   77

   78             embed = true;

   79           }

   80           else

   81           {

   82             // Ask whether to embed or link

   83

   84             PromptKeywordOptions pko =

   85               new PromptKeywordOptions(

   86                 "\nEmbed or link the attribute values: "

   87               );

   88

   89             pko.AllowNone = true;

   90             pko.Keywords.Add("Embed");

   91             pko.Keywords.Add("Link");

   92             pko.Keywords.Default = "Embed";

   93             PromptResult pkr =

   94               ed.GetKeywords(pko);

   95

   96             if (pkr.Status == PromptStatus.None ||

   97                 pkr.Status == PromptStatus.OK)

   98             {

   99               if (pkr.Status == PromptStatus.None ||

  100                   pkr.StringResult == "Embed")

  101                 embed = true;

  102               else

  103                 embed = false;

  104             }

  105           }

  106

  107           Transaction tr =

  108             doc.TransactionManager.StartTransaction();

  109           using (tr)

  110           {

  111             // Let's check the block exists

  112

  113             BlockTable bt =

  114               (BlockTable)tr.GetObject(

  115                 doc.Database.BlockTableId,

  116                 OpenMode.ForRead

  117               );

  118

  119             if (!bt.Has(blockToFind))

  120             {

  121               ed.WriteMessage(

  122                 "\nBlock "

  123                 + blockToFind

  124                 + " does not exist."

  125               );             

  126             }

  127             else

  128             {

  129               // And go through looking for

  130               // attribute definitions

  131

  132               StringCollection colNames =

  133                 new StringCollection();

  134               int colToTotalIdx = -1;

  135

  136               BlockTableRecord bd =

  137                 (BlockTableRecord)tr.GetObject(

  138                   bt[blockToFind],

  139                   OpenMode.ForRead

  140                 );

  141               foreach (ObjectId adId in bd)

  142               {

  143                 DBObject adObj =

  144                   tr.GetObject(

  145                     adId,

  146                     OpenMode.ForRead

  147                   );

  148

  149                 // For each attribute definition we find...

  150

  151                 AttributeDefinition ad =

  152                   adObj as AttributeDefinition;

  153                 if (ad != null)

  154                 {

  155                   // ... we add its name to the list

  156

  157                   colNames.Add(ad.Tag);

  158

  159                   if (ad.Tag.ToUpper() == columnToTotal)

  160                   {

  161                     // Save the index of the column

  162                     // we want to total

  163

  164                     colToTotalIdx =

  165                       colNames.Count - 1;

  166                   }

  167                 }

  168               }

  169               // If we didn't find the attribute to be totalled

  170               // then simply ignore the request and continue

  171

  172               if (columnToTotal != "" && colToTotalIdx < 0)

  173               {

  174                 ed.WriteMessage(

  175                   "\nAttribute definition for "

  176                   + columnToTotal

  177                   + " not found in "

  178                   + blockToFind

  179                   + ". Total will not be added to the table."

  180                 );

  181               }

  182               if (colNames.Count == 0)

  183               {

  184                 ed.WriteMessage(

  185                   "\nThe block "

  186                   + blockToFind

  187                   + " contains no attribute definitions."                  

  188                 );

  189               }

  190               else

  191               {

  192                 // Ask the user for the insertion point

  193                 // and then create the table

  194

  195                 PromptPointResult ppr =

  196                   ed.GetPoint(

  197                     "\nEnter table insertion point: "

  198                   );

  199

  200                 if (ppr.Status == PromptStatus.OK)

  201                 {

  202                   Table tb = new Table();

  203                   tb.TableStyle = db.Tablestyle;

  204                   tb.NumRows = 1;

  205                   tb.NumColumns = colNames.Count;

  206                   tb.SetRowHeight(rowHeight);

  207                   tb.SetColumnWidth(colWidth);

  208                   tb.Position = ppr.Value;

  209

  210                   // Let's add our column headings

  211

  212                   for (int i = 0; i < colNames.Count; i++)

  213                   {

  214                     SetCellText(tb, 0, i, colNames[i]);

  215                   }

  216

  217                   // Now let's search for instances of

  218                   // our block in the modelspace

  219

  220                   BlockTableRecord ms =

  221                     (BlockTableRecord)tr.GetObject(

  222                       bt[BlockTableRecord.ModelSpace],

  223                       OpenMode.ForRead

  224                     );

  225

  226                   int rowNum = 1;

  227                   foreach (ObjectId objId in ms)

  228                   {

  229                     DBObject obj =

  230                       tr.GetObject(

  231                         objId,

  232                         OpenMode.ForRead

  233                       );

  234                     BlockReference br =

  235                       obj as BlockReference;

  236                     if (br != null)

  237                     {

  238                       BlockTableRecord btr =

  239                         (BlockTableRecord)tr.GetObject(

  240                           br.BlockTableRecord,

  241                           OpenMode.ForRead

  242                         );

  243                       using (btr)

  244                       {

  245                         if (btr.Name.ToUpper() == blockToFind)

  246                         {

  247                           // We have found one of our blocks,

  248                           // so add a row for it in the table

  249

  250                           tb.InsertRows(

  251                               rowNum,

  252                               rowHeight,

  253                               1

  254                           );

  255

  256                           // Assume that the attribute refs

  257                           // follow the same order as the

  258                           // attribute defs in the block

  259

  260                           int attNum = 0;

  261                           foreach (

  262                             ObjectId arId in

  263                             br.AttributeCollection

  264                           )

  265                           {

  266                             DBObject arObj =

  267                               tr.GetObject(

  268                                 arId,

  269                                 OpenMode.ForRead

  270                               );

  271                             AttributeReference ar =

  272                               arObj as AttributeReference;

  273                             if (ar != null)

  274                             {

  275                               // Embed or link the values

  276

  277                               string strCell;

  278                               if (embed)

  279                               {

  280                                 strCell = ar.TextString;

  281                               }

  282                               else

  283                               {

  284                                 string strArId =

  285                                   arId.ToString();

  286                                 strArId =

  287                                   strArId.Trim(

  288                                     new char[] { '(', ')' }

  289                                   );

  290                                 strCell =

  291                                   "%<\\AcObjProp Object("

  292                                     + "%<\\_ObjId "

  293                                     + strArId

  294                                     + ">%).TextString>%";

  295                               }

  296                               SetCellText(

  297                                 tb,

  298                                 rowNum,

  299                                 attNum,

  300                                 strCell

  301                               );

  302                             }

  303                             attNum++;

  304                           }

  305                           rowNum++;

  306                         }

  307                       }

  308                     }

  309                   }

  310

  311                   // Now let's add a row for our total

  312

  313                   if (colToTotalIdx >= 0)

  314                   {

  315                     tb.InsertRows(rowNum, rowHeight, 1);

  316                     char colLetter =

  317                       Convert.ToChar(

  318                         (Convert.ToInt32(

  319                           'A') + colToTotalIdx

  320                         )

  321                       );

  322

  323                     // Add a formula to sum the column

  324

  325                     SetCellText(

  326                       tb,

  327                       rowNum,

  328                       colToTotalIdx,

  329                       "%<\\AcExpr (Sum("

  330                         + colLetter

  331                         + "2:"

  332                         + colLetter

  333                         + rowNum.ToString()

  334                         + ")) \\f  \"%lu2%pr2\">%"

  335                     );

  336                   }

  337                   tb.GenerateLayout();

  338

  339                   ms.UpgradeOpen();

  340                   ms.AppendEntity(tb);

  341                   tr.AddNewlyCreatedDBObject(tb, true);

  342                   tr.Commit();

  343                   ed.Regen();