September 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        








« The ADN program opens its doors | Main | Creating a motion-detecting security cam with a Raspberry Pi – Part 2 »

September 03, 2012

A handy jig for creating AutoCAD text using .NET – Part 4

After the first three parts of this series covered the basic jig that makes use of the standard keywords mechanism to adjust text’s style and rotation as it’s being placed, it eventually made sense to implement the remaining requirement initially provided for the jig: this post looks at different approaches for having the jig respond to single keystrokes rather than full keyword inputs.

Dave Osborne very helpfully got me started on this by providing an initial implementation that makes use of an IMessageFilter – something he’d apparently gleaned from this previous post. Thanks, Dave! :-)

All the approaches I’ll outline in this post make use of this core technique, but do so in slightly different ways. Basically we want our jig to now respond to the Tab key to rotate our text by 90 degrees – it would be simple enough to extend the technique to cover different properties, too, but that’s left as an exercise for the reader.

The trick is that we have three separate classes between which we will need to communicate, primarily to adjust the angle: our Commands class, our Jig class and our IMessageFilter class.

The first thing you might try when getting them to share information between such classes is to have a static member of the Commands class that gets accessed by the other two. This is dangerous, mainly because shared data is tricky to manage. If you have a second document, for instance, which also has the same command running, you may well hit a problem if they both access and adjust the same “angle” value. They won’t be able to do so at exactly the same time – as AutoCAD isn’t multi-threaded – but the results are likely to be unpredictable. See this previous post for more on this topic.

You could also keep the data in the Commands class, but this time expose it in a different way to the other classes. For instance, you might choose to have a public property exposed and then pass a reference to the Commands class when creating the Jig and the IMessageFilter, so that their implementations might access the data.

Or you might decouple the implementations even further and define delegates for the rotate action and a method to access the angle property’s current value. You could then pass lambda functions in from the Commands class, and the bodies of these lambdas could very validly access a local variable in the Commands class, so you wouldn’t even need object-level state exposed.

In any of these three approaches you end up with an IMessageFilter object that modifies the angle directly, rather than passing through the jig. They work well enough when the Jig is processing messages – such as when you hit Tab as you’re dragging the mouse – but work less well when the mouse is stationary. It’s only when the mouse moves that you’ll see the effects of hitting the Tab key catch up with the object’s on-screen rotation.

Which has led me to my preferred implementation: simply using the IMessageFilter as a “keyboard accelerator” class that sends the assigned keyword through to the Jig for processing. This has the benefit of consistency – you can keep the keyword implementation intact, and the user can also use that rather than the Tab key – and also of responsiveness – there are no discrepancies between the object state and the on-screen representation.

Here’s the C# implementation, with the new lines in red, although I’ve made a few other largely cosmetic but unhighlighted changes such as adding a namespace (and here’s the source file for you to download).

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.DatabaseServices;

    3 using Autodesk.AutoCAD.EditorInput;

    4 using Autodesk.AutoCAD.Geometry;

    5 using Autodesk.AutoCAD.GraphicsInterface;

    6 using Autodesk.AutoCAD.Runtime;

    7 using System.Runtime.InteropServices;

    8 using WinForms = System.Windows.Forms;

    9 using System;

   10 

   11 namespace QuickText

   12 {

   13   public class Commands

   14   {

   15     [CommandMethod("QT")]

   16     static public void QuickText()

   17     {

   18       Document doc =

   19         Application.DocumentManager.MdiActiveDocument;

   20       Database db = doc.Database;

   21       Editor ed = doc.Editor;

   22 

   23       PromptStringOptions pso =

   24         new PromptStringOptions("\nEnter text string");

   25       pso.AllowSpaces = true;

   26       PromptResult pr = ed.GetString(pso);

   27 

   28       if (pr.Status != PromptStatus.OK)

   29         return;

   30 

   31       Transaction tr =

   32         doc.TransactionManager.StartTransaction();

   33       using (tr)

   34       {

   35         BlockTableRecord btr =

   36           (BlockTableRecord)tr.GetObject(

   37             db.CurrentSpaceId, OpenMode.ForWrite

   38           );

   39 

   40         // Create the text object, set its normal and contents

   41 

   42         DBText txt = new DBText();

   43         txt.Normal =

   44           ed.CurrentUserCoordinateSystem.

   45             CoordinateSystem3d.Zaxis;

   46         txt.TextString = pr.StringResult;

   47 

   48         // We'll add the text to the database before jigging

   49         // it - this allows alignment adjustments to be

   50         // reflected

   51 

   52         btr.AppendEntity(txt);

   53         tr.AddNewlyCreatedDBObject(txt, true);

   54 

   55         // Create our jig

   56 

   57         TextPlacementJig pj =

   58           new TextPlacementJig(tr, db, txt);

   59 

   60         // Loop as we run our jig, as we may have keywords

   61 

   62         PromptStatus stat = PromptStatus.Keyword;

   63         while (stat == PromptStatus.Keyword)

   64         {

   65           var filt = new TxtRotMsgFilter(doc);

   66 

   67           WinForms.Application.AddMessageFilter(filt);

   68           PromptResult res = ed.Drag(pj);

   69           WinForms.Application.RemoveMessageFilter(filt);

   70 

   71           stat = res.Status;

   72           if (

   73             stat != PromptStatus.OK &&

   74             stat != PromptStatus.Keyword

   75           )

   76             return;

   77         }

   78 

   79         tr.Commit();

   80       }

   81     }

   82 

   83     private class TextPlacementJig : EntityJig

   84     {

   85       // Declare some internal state

   86 

   87       private Database _db;

   88       private Transaction _tr;

   89       private Point3d _position;

   90       private double _angle, _txtSize;

   91       private bool _toggleBold, _toggleItalic;

   92       private TextHorizontalMode _align;

   93 

   94       // Constructor

   95 

   96       public TextPlacementJig(

   97         Transaction tr, Database db, Entity ent

   98       ) : base(ent)

   99       {

  100         _db = db;

  101         _tr = tr;

  102         _angle = 0;

  103         _txtSize = 1;

  104       }

  105 

  106       protected override SamplerStatus Sampler(

  107         JigPrompts jp

  108       )

  109       {

  110         // We acquire a point but with keywords

  111 

  112         JigPromptPointOptions po =

  113           new JigPromptPointOptions(

  114             "\nPosition of text"

  115           );

  116 

  117         po.UserInputControls =

  118           (UserInputControls.Accept3dCoordinates |

  119             UserInputControls.NullResponseAccepted |

  120             UserInputControls.NoNegativeResponseAccepted |

  121             UserInputControls.GovernedByOrthoMode);

  122 

  123         po.SetMessageAndKeywords(

  124           "\nSpecify position of text or " +

  125           "[Bold/Italic/LArger/Smaller/" +

  126             "ROtate90/LEft/Middle/RIght]: ",

  127           "Bold Italic LArger Smaller " +

  128           "ROtate90 LEft Middle RIght"

  129         );

  130 

  131         PromptPointResult ppr = jp.AcquirePoint(po);

  132 

  133         if (ppr.Status == PromptStatus.Keyword)

  134         {

  135           switch (ppr.StringResult)

  136           {

  137             case "Bold":

  138               {

  139                 _toggleBold = true;

  140                 break;

  141               }

  142             case "Italic":

  143               {

  144                 _toggleItalic = true;

  145                 break;

  146               }

  147             case "LArger":

  148               {

  149                 // Multiple the text size by two

  150 

  151                 _txtSize *= 2;

  152                 break;

  153               }

  154             case "Smaller":

  155               {

  156                 // Divide the text size by two

  157 

  158                 _txtSize /= 2;

  159                 break;

  160               }

  161             case "ROtate90":

  162               {

  163                 // To rotate clockwise we subtract 90 degrees &

  164                 // then normalise the angle between 0 and 360

  165 

  166                 _angle -= Math.PI / 2;

  167                 while (_angle < Math.PI * 2)

  168                 {

  169                   _angle += Math.PI * 2;

  170                 }

  171                 break;

  172               }

  173             case "LEft":

  174               {

  175                 _align = TextHorizontalMode.TextLeft;

  176                 break;

  177               }

  178             case "RIght":

  179               {

  180                 _align = TextHorizontalMode.TextRight;

  181                 break;

  182               }

  183             case "Middle":

  184               {

  185                 _align = TextHorizontalMode.TextMid;

  186                 break;

  187               }

  188           }

  189 

  190           return SamplerStatus.OK;

  191         }

  192         else if (ppr.Status == PromptStatus.OK)

  193         {

  194           // Check if it has changed or not (reduces flicker)

  195 

  196           if (

  197             _position.DistanceTo(ppr.Value) <

  198               Tolerance.Global.EqualPoint

  199           )

  200             return SamplerStatus.NoChange;

  201 

  202           _position = ppr.Value;

  203           return SamplerStatus.OK;

  204         }

  205 

  206         return SamplerStatus.Cancel;

  207       }

  208 

  209       protected override bool Update()

  210       {

  211         // Set properties on our text object

  212 

  213         DBText txt = (DBText)Entity;

  214 

  215         txt.Position = _position;

  216         txt.Height = _txtSize;

  217         txt.Rotation = _angle;

  218         txt.HorizontalMode = _align;

  219         if (_align != TextHorizontalMode.TextLeft)

  220         {

  221           txt.AlignmentPoint = _position;

  222           txt.AdjustAlignment(_db);

  223         }

  224 

  225         // Set the bold and/or italic properties on the style

  226 

  227         if (_toggleBold || _toggleItalic)

  228         {

  229           TextStyleTable tab =

  230             (TextStyleTable)_tr.GetObject(

  231               _db.TextStyleTableId, OpenMode.ForRead

  232             );

  233 

  234           TextStyleTableRecord style =

  235             (TextStyleTableRecord)_tr.GetObject(

  236               txt.TextStyleId, OpenMode.ForRead

  237             );

  238 

  239           // A bit convoluted, but this check will tell us

  240           // whether the new style is bold/italic

  241 

  242           bool bold = !(style.Font.Bold == _toggleBold);

  243           bool italic = !(style.Font.Italic == _toggleItalic);

  244           _toggleBold = false;

  245           _toggleItalic = false;

  246 

  247           // Get the new style name based on the old name and

  248           // a suffix ("_BOLD", "_ITALIC" or "_BOLDITALIC")

  249 

  250           var oldName = style.Name.Split(new[] { '_' });

  251           string newName =

  252             oldName[0] +

  253             (bold || italic ? "_" +

  254               (bold ? "BOLD" : "") +

  255               (italic ? "ITALIC" : "")

  256               : "");

  257 

  258           // We only create a duplicate style if one doesn't

  259           // already exist

  260 

  261           if (tab.Has(newName))

  262           {

  263             txt.TextStyleId = tab[newName];

  264           }

  265           else

  266           {

  267             // We have to create a new style - clone it

  268 

  269             TextStyleTableRecord newStyle =

  270               (TextStyleTableRecord)style.Clone();

  271 

  272             // Set a new name to avoid duplicate keys

  273 

  274             newStyle.Name = newName;

  275 

  276             // Create a new font based on the old one, but with

  277             // our values for bold & italic

  278 

  279             FontDescriptor oldFont = style.Font;

  280             FontDescriptor newFont =

  281               new FontDescriptor(

  282                 oldFont.TypeFace, bold, italic,

  283                 oldFont.CharacterSet, oldFont.PitchAndFamily

  284               );

  285 

  286             // Set it on the style

  287 

  288             newStyle.Font = newFont;

  289 

  290             // Add the new style to the text style table and

  291             // the transaction

  292 

  293             tab.UpgradeOpen();

  294             ObjectId styleId = tab.Add(newStyle);

  295             _tr.AddNewlyCreatedDBObject(newStyle, true);

  296 

  297             // And finally set the new style on our text object

  298 

  299             txt.TextStyleId = styleId;

  300           }

  301         }

  302 

  303         return true;

  304       }

  305     }

  306   }

  307 

  308   public class TxtRotMsgFilter : WinForms.IMessageFilter

  309   {

  310     [DllImport(

  311       "user32.dll",

  312       CharSet = CharSet.Auto,

  313       ExactSpelling = true

  314       )]

  315     public static extern short GetKeyState(int keyCode);

  316 

  317     const int WM_KEYDOWN = 256;

  318     const int VK_CONTROL = 17;

  319 

  320     private Document _doc = null;

  321 

  322     public TxtRotMsgFilter(Document doc)

  323     {

  324       _doc = doc;

  325     }

  326 

  327     public bool PreFilterMessage(ref WinForms.Message m)

  328     {

  329       if (

  330         m.Msg == WM_KEYDOWN &&

  331         m.WParam == (IntPtr)WinForms.Keys.Tab &&

  332         GetKeyState(VK_CONTROL) >= 0

  333       )

  334       {

  335         _doc.SendStringToExecute("_RO ", true, false, false);

  336         return true;

  337       }

  338       return false;

  339     }

  340   }

  341 }

Something to note from this implementation… to avoid responding to Control-Tab – which should switch between open drawings, of course – the code detects whether the Control key has been pressed at the same time as Tab. It only sends the _RO keyword to the command-line in the cases where the Control key is not pressed. It seems Alt-Tab is intercepted before it gets to AutoCAD – which makes sense, as it’s used to switch between applications – so that’s not something we need to check for.

To test it all works well, it’s interesting to have two new drawings open – with the application loaded – and launch the QT command in each, entering a different text string. You can then Control-Tab between the drawings, using the Tab key to rotate them independently.

Update:

Thanks to Heinz Dober for reminding me that at additional assembly reference to “System.Windows.Forms” will be needed in your project for the IMessageFilter-related code to compile.

blog comments powered by Disqus

Feed/Share

10 Random Posts