Kean Walmsley


  • About the Author
    Kean on Google+

August 2014

Sun Mon Tue Wed Thu Fri Sat
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            








« Implementing a “Quick SaveAs” command in AutoCAD using .NET | Main | AU 2009: Attend one of my sessions from the comfort of your armchair »

October 23, 2009

Adding items to an AutoCAD tool palette using .NET

This post carries directly on from the last one, which implemented a rudimentary “Quick SaveAs” capability in AutoCAD. Much of the explanation behind the design of today’s code is there, so please do read it first (if you haven’t already).

Today we’re taking the code further by automatically creating an item on a tool palette with the thumbnail of the recently-saved drawing which, when used, will run a script created when we saved the drawing.

Here’s the updated code with the new or modified lines in red. You can download the C# source file from here.

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.DatabaseServices;

    3 using Autodesk.AutoCAD.EditorInput;

    4 using Autodesk.AutoCAD.Runtime;

    5 using Autodesk.AutoCAD.Windows.ToolPalette;

    6 using System.Runtime.InteropServices;

    7 using System.IO;

    8 using System;

    9 

   10 namespace QuickSaveAs

   11 {

   12   public class Commands

   13   {

   14     // Set up static variable for the path to our folder

   15     // of drawings, as well as the base filename and a

   16     // counter to make the unique filename

   17 

   18     static string _path = "",

   19                   _base = "";

   20     static int _count = 0;

   21 

   22     // Various filename and path-related constants

   23 

   24     const string sfxSep = " ",

   25                 extSep = ".",

   26                 pthSep = "\\",

   27                 lspSep = "/",

   28                 dwgExt = ".dwg",

   29                 scrExt = ".txt",

   30                 bmpExt = ".bmp",

   31                 bmpLoc = "Images",

   32                 scrLoc = "Scripts";

   33 

   34     // Our QuickSaveAs command

   35 

   36     [CommandMethod("QSAVEAS")]

   37     public void QuickSaveAs()

   38     {

   39       Document doc =

   40         Application.DocumentManager.MdiActiveDocument;

   41       Editor ed = doc.Editor;

   42       Database db = doc.Database;

   43 

   44       // If this is the first time run...

   45 

   46       if (_path == "" || _base == "")

   47       {

   48         // Ask the user for a base file location

   49 

   50         PromptSaveFileOptions opts =

   51           new PromptSaveFileOptions(

   52             "Select location to save first drawing file"

   53           );

   54         opts.Filter = "Drawing (*.dwg)|*.dwg";

   55         PromptFileNameResult pr =

   56           ed.GetFileNameForSave(opts);

   57 

   58         // Delete the file, if it exists

   59         // (may be a problem if the file is in use)

   60 

   61         if (File.Exists(pr.StringResult))

   62         {

   63           try

   64           {

   65             File.Delete(pr.StringResult);

   66           }

   67           catch { }

   68         }

   69 

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

   71         {

   72           // If a file was selected, and it contains a path...

   73 

   74           if (pr.StringResult.Contains(pthSep))

   75           {

   76             // Separate the path from the file name

   77 

   78             int idx = pr.StringResult.LastIndexOf(pthSep);

   79             _path =

   80               pr.StringResult.Substring(0, idx);

   81             string fullname =

   82               pr.StringResult.Substring(idx+1);

   83 

   84             // If the path has an extension (this should always

   85             // be the case), extract the base file name

   86 

   87             if (fullname.Contains(extSep))

   88             {

   89               _base =

   90                 fullname.Substring(

   91                   0,

   92                   fullname.LastIndexOf(extSep)

   93                 );

   94 

   95               // Create folders for our icons and our scripts

   96 

   97               Directory.CreateDirectory(

   98                 _path + pthSep + bmpLoc

   99               );

  100               Directory.CreateDirectory(

  101                 _path + pthSep + scrLoc

  102               );

  103             }

  104           }

  105         }

  106       }

  107 

  108       // Assuming the path and name were set appropriately...

  109 

  110       if (_path != "" && _base != "")

  111       {

  112         string name = _base;

  113 

  114         // Add our suffix if not the first time run

  115 

  116         if (_count > 0)

  117           name += sfxSep + _count.ToString();

  118 

  119         // Our drawing is located in the base path

  120 

  121         string dwgPath = _path + pthSep + name + dwgExt;

  122 

  123         // While our script is in a sub-folder

  124 

  125         string scrPath =

  126           _path + pthSep + scrLoc + pthSep + name + scrExt;

  127 

  128         // Create a dummy script, so we can make sure we pick

  129         // up the contents in our dummy execute command

  130 

  131         File.WriteAllText(

  132           scrPath,

  133           "This is a dummy script for " + name + "."

  134         );

  135 

  136         // Now we want to save our drawing and use the image

  137         // for our tool icon

  138 

  139         // Using either COM or .NET doesn't generate a

  140         // thumbnail in the resultant file (or its Database)

  141 

  142         // .NET:

  143         // db.SaveAs(dwgPath, false, DwgVersion.Current, null);

  144 

  145         // COM:

  146         // AcadDocument adoc = (AcadDocument)doc.AcadDocument;

  147         // adoc.SaveAs(dwgPath, AcSaveAsType.acNative, null);

  148 

  149         // So we'll send commands to the command-line

  150         // We'll use LISP, to avoid having to set FILEDIA to 0

  151 

  152         object ocmd = Application.GetSystemVariable("CMDECHO");

  153         string dwgPath2 = dwgPath.Replace(pthSep, lspSep);

  154         string scrPath2 = scrPath.Replace(pthSep, lspSep);

  155 

  156         string c1 =

  157           "(setvar \"CMDECHO\" 0)" +

  158           "(command \"_.SAVEAS\" \"\" \"" + dwgPath2 + "\")";

  159         string c2 =

  160           "(setvar \"CMDECHO\" " + ocmd.ToString() + ")" +

  161           "(tp-create \"" + name + "\" \"" + scrPath2 + "\")" +

  162           "(princ) ";

  163         string cmd = c1 + c2;

  164 

  165         if (cmd.Length <= 255)

  166         {

  167           doc.SendStringToExecute(cmd, false, false, false);

  168         }

  169         else

  170         {

  171           doc.SendStringToExecute(c1+" ", false, false, false);

  172           doc.SendStringToExecute(c2, false, false, false);

  173         }

  174 

  175         // Print a confirmation message for the DWG save

  176         // (which actually gets displayed before the queued

  177         // string gets executed, but anyway)

  178 

  179         ed.WriteMessage("\nSaved to: \"" + dwgPath + "\"");

  180 

  181         _count++;

  182       }

  183     }

  184 

  185     // Our LISP-registered continuation function to create a

  186     // command tool on our tool palette

  187 

  188     [LispFunction("TP-CREATE")]

  189     public ResultBuffer CreateToolPaletteCommand(

  190       ResultBuffer rb

  191     )

  192     {

  193       const int RTSTR = 5005;

  194 

  195       Document doc =

  196         Application.DocumentManager.MdiActiveDocument;

  197       Editor ed = doc.Editor;

  198 

  199       if (rb == null)

  200       {

  201         ed.WriteMessage("\nError: too few arguments.");

  202       }

  203       else

  204       {

  205         // We're only interested in the first two arguments

  206 

  207         Array args = rb.AsArray();

  208         if (args.Length != 2)

  209         {

  210           ed.WriteMessage(

  211             "\nError: wrong number of arguments."

  212           );

  213         }

  214         else

  215         {

  216           // First argument is the name, second is the path

  217           // to the script

  218 

  219           TypedValue tv1 = (TypedValue)args.GetValue(0);

  220           TypedValue tv2 = (TypedValue)args.GetValue(1);

  221 

  222           if (tv1 != null && tv1.TypeCode == RTSTR &&

  223               tv2 != null && tv2.TypeCode == RTSTR)

  224           {

  225             string name = Convert.ToString(tv1.Value);

  226             string lspScrPath = Convert.ToString(tv2.Value);

  227             string scrPath =

  228               lspScrPath.Replace(lspSep, pthSep);

  229             bool success =

  230               CreateCommand(doc.Database, name, scrPath);

  231             return

  232               (success ?

  233                 new ResultBuffer(

  234                   new TypedValue(RTSTR, tv1.Value)

  235                 )

  236                 : null);

  237           }

  238         }

  239       }

  240       return null;

  241     }

  242 

  243     // Function to add a command tool to our tool palette to

  244     // execute the script

  245 

  246     private bool CreateCommand(

  247       Database db,

  248       string name,

  249       string scrPath

  250     )

  251     {

  252       const string catName = "ScriptCatalog";

  253       const string palName = "Scripts";

  254 

  255       ToolPaletteManager tpm = ToolPaletteManager.Manager;

  256 

  257       // Get the GUID of our dummy custom tool

  258 

  259       Type t = typeof(DummyTool);

  260       GuidAttribute ga =

  261         (GuidAttribute)t.GetCustomAttributes(

  262           typeof(GuidAttribute), false)[0];

  263       Guid g = new Guid(ga.Value);

  264 

  265       // Instanciate our dummy tool - this will allow us to use

  266       // its helper functions

  267 

  268       DummyTool tool = new DummyTool();

  269       Catalog cat;

  270       Palette pal = null;

  271 

  272       // First we check whether our GUID is in a catalog

  273 

  274       CatalogItem ci = tpm.StockToolCatalogs.Find(g);

  275       if (ci != null)

  276       {

  277         // If it is, search each catalog for our palette

  278 

  279         foreach(CatalogItem ci2 in tpm.Catalogs)

  280         {

  281           for (int i = 0; i < ci2.ChildCount; i++)

  282           {

  283             CatalogItem ci3 = ci2.GetChild(i);

  284             if (ci3 != null && ci3.Name == palName)

  285             {

  286               pal = ci3 as Palette;

  287               break;

  288             }

  289           }

  290           if (pal != null)

  291             break;

  292         }

  293       }

  294 

  295       // If we didn't find our palette, create it

  296 

  297       if (pal == null)

  298       {

  299         cat = tool.CreateStockTool(catName);

  300         pal = tool.CreatePalette(cat, palName);

  301       }

  302 

  303       // To add our command tool instance we need an icon

  304 

  305       ImageInfo ii = new ImageInfo();

  306       if (db.ThumbnailBitmap != null)

  307       {

  308         // Which we create from the Database's thumbnail

  309 

  310         string bmpPath =

  311           _path + pthSep + bmpLoc + pthSep + name + bmpExt;

  312         db.ThumbnailBitmap.Save(bmpPath);

  313         ii.ResourceFile = bmpPath;

  314       }

  315       ii.Size = new System.Drawing.Size(65, 65);

  316 

  317       // And then we use our dummy tool to create the

  318       // command tool

  319 

  320       tool.CreateCommandTool(

  321         pal,

  322         name,

  323         ii,

  324         "_EXECSCR \"" + scrPath.Replace(pthSep, lspSep) + "\""

  325       );

  326 

  327       // Finally we reload the catalogs to display the change

  328 

  329       tpm.LoadCatalogs();

  330 

  331       return true;

  332     }

  333 

  334     // A dummy command to simulate the execution of our script

  335     // (which simply reads the contents and displays them on

  336     // the command-line)

  337 

  338     [CommandMethod("EXECSCR")]

  339     public void ExecuteScript()

  340     {

  341       Document doc =

  342         Application.DocumentManager.MdiActiveDocument;

  343       Editor ed = doc.Editor;

  344 

  345       PromptResult pr =

  346         ed.GetString(

  347           "\nEnter location of script to execute: "

  348         );

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

  350       {

  351         string path =

  352           pr.StringResult.Replace(lspSep, pthSep);

  353         if (File.Exists(path))

  354         {

  355           string contents = File.ReadAllText(path);

  356           ed.WriteMessage(

  357             "\nDummy script contained: \"{0}\"",

  358             contents

  359           );

  360         }

  361       }

  362     }

  363   }

  364 

  365   // Our dummy tool which simply derives from CustomToolBase

  366   // (there may be a more straightforward way to get access

  367   // to the helpers in CustomToolBase, but anyway)

  368 

  369   [Guid("3B725500-0451-4081-A1BB-B37CE6A65767")]

  370   [Tool("MyDummyTool", "IDB_TOOL")]

  371   [ClassInterface(ClassInterfaceType.AutoDual)]

  372   public class DummyTool : CustomToolBase

  373   {

  374   }

  375 }

Some notes on the changes:

  • Lines 5, 6 and 8 add some additional namespaces.
    • It's worth noting that you'll need to add an assembly reference to AcTcMgd (depending on the version of AutoCAD you're using), for the updated code to build.
  • Lines 28-32 add some additional constants related to scripts and icon images.
  • Lines 95-102 create additional directories for our scripts and images.
  • Lines 123-134 create a dummy script when we save a drawing.
  • Lines 154-173 deal with a limitation we have with sending strings to the command-line:
    • AutoCAD’s command-line input buffer is only 255 characters in size, so if our string is longer (because of a deep file path), we send it in two pieces, terminating the first with a space character. It’s still possible that really deep paths could cause a problem with this code, but splitting the string further is left as an exercise for the reader. :-)
    • The string also calls a new continuation function (registered via LISP, see below) to create an item on our tool palette.
  • Lines 185-241 register a continuation function via LISP, so we can get control back in our code once the SAVEAS command has completed.
  • Lines 243-332 define a function to create an item on our tool palette. This function is called from the above LISP function.
  • Lines 334-363 simulate the execution of a script, so that when our tool palette is used, something happens.
    • The command simply reads in the contents of the "script" file and prints the contents to the command-line.
  • Lines 365-374 define a dummy custom tool, which we use as a shortcut for certain tool palette-related operations.

Now let’s take a look at the results of running this. In the last post we saw an example where a number of drawings get created in a particular folder. If we perform the same operations with this code, the same things happen (no need to show the drawings or the command-line output, they should be the same), but in addition we see a tool palette populated with images of our model at various stages:

Our QSaveAs-populated tool palette

You may have to right-click the stacked tool palette tabs to locate the “Scripts” tool palette (I haven’t found a way of doing this automatically, as it wasn’t really essential for my particular application).

When we select the items on the tool palette in sequence, we see our EXECSCR command is called with the location of the script created for each DWG file, which then gets read and printed to the command-line:

Command: _EXECSCR

Enter location of script to execute:

"C:/QSaveAs Test/Scripts/Solid model.txt"

Dummy script contained: "This is a dummy script for Solid model."

Command: _EXECSCR

Enter location of script to execute:

"C:/QSaveAs Test/Scripts/Solid model 1.txt"

Dummy script contained: "This is a dummy script for Solid model 1."

Command: _EXECSCR

Enter location of script to execute:

"C:/QSaveAs Test/Scripts/Solid model 2.txt"

Dummy script contained: "This is a dummy script for Solid model 2."

Command: _EXECSCR

Enter location of script to execute:

"C:/QSaveAs Test/Scripts/Solid model 3.txt"

Dummy script contained: "This is a dummy script for Solid model 3."

Command: _EXECSCR

Enter location of script to execute:

"C:/QSaveAs Test/Scripts/Solid model 4.txt"

Dummy script contained: "This is a dummy script for Solid model 4."

Command: _EXECSCR

Enter location of script to execute:

"C:/QSaveAs Test/Scripts/Solid model 5.txt"

Dummy script contained: "This is a dummy script for Solid model 5."

While clearly not actually doing anything useful, the script file that we’ve created (and I don’t mean script in the AutoCAD sense of the term – I’m using it in a more generic sense) could actually be regenerating the drawing contents (for instance). With a little more work. :-)

Update:

This post shows a streamlined approach for this application for AutoCAD 2010 and above.

blog comments powered by Disqus

10 Random Posts