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     {

 
0; 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] +

< p style="margin: 0px">  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         &#
160;   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.

24 responses to “A handy jig for creating AutoCAD text using .NET – Part 4”

  1. I get an error message
    Ich bekomme eine Fehlermeldung

    Fehler1Der Typ- oder Namespacename "Forms" ist im Namespace "System.Windows" nicht vorhanden. (Fehlt ein Assemblyverweis?)

  2. Hi Heinz,

    Right - I should have mentioned your project will need an assembly reference to "System.Windows.Forms" to be able to use an IMessageFilter.

    Thanks for the reminder!

    Kean

  3. Please gladly
    We will provide an update

    Bitte gern
    Wir es ein Update geben

  4. 'm Sorry beginners how to do it
    Thank you

    Bin leider Anfänger wie mache ich das
    Danke

  5. If you right-click on the project in the Solution Explorer, you can choose "Add Reference". Then go to the .NET tab and look for System.Windows.Forms in the list. Select it, click Add and the reference should now be in your project.

    Kean

  6. Super Thank you

    Super Danke

  7. Responding to keypress events while jigging is much simpler than your solution implies, and requires no IMessageFilter.

    Have a look at the MSDN docs for GetAsyncKeyState() API.

  8. Interesting - thanks, Tony. I'd overlooked the fact that Sampler() would get called for individual keypresses, too.

    That said, I didn't have much luck getting it to behave reliably, though: as the Jig gets multiple calls during a typical keypress event (as multiple Windows messages are generated while the key is held down) it's hard to separate the press from the hold, as it were.

    Checking the LSB of the value returned by GetAsyncKeyState() is meant to help - it's supposed to indicate whether they key was pressed since the last call - but even that doesn't appear to work reliably (and is even documented as being mainly for 16-bit compatibility).

    So far the message-filter checking for an actual keypress event seems much more solid. Perhaps you've had a different experience, though (and have successfully implemented this where I've so far failed).

    Cheers,

    Kean

  9. The issues with the LSB in GetAsyncKeyState() noted in the docs are mainly with preemptive multithreaded apps. And, if the sampler function is getting called on keypress events, then the keys should be pressed when either GetKeyState() or GetAsyncKeyState() are called,.

    But after looking more closely at your post, I don't think GetAsyncKeyState() is a solution:

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

    IMessageFilter is not a 'class', it's an [b]interface[/b], that can be implemented on any non-static class, like the one that you derive from EntityJig:

    private class TextPlacementJig : EntityJig, IMessageFilter
    {
    .....
    }

  10. I agree that the documented issue with the LSB is not what's causing our particular problem, but the fact remains that it didn't work adequately in this scenario.

    And where I said "our IMessageFilter class" I might also have said "our class implementing the IMessageFilter interface", of course. Sorry if that wasn't clear.

    We could certainly have a more tightly coupled implementation - having the Jig expose the IMessageFilter interface - but that would only solve the communication between the IMessageFilter and the Jig: if we adjusted internal state from the PreFilterMessage() function it would still not get displayed until Sampler() was called... so we'd still be best placed to use SendStringToExecute() to drive the code behind the keyword (ultimately via Sampler()). Which means we actually may as well have them in independent classes (which would certainly simplify removing that particular behaviour).

    Kean

  11. "We could certainly have a more tightly coupled implementation - having the Jig expose the IMessageFilter interface - but that would only solve the communication between the IMessageFilter and the Jig: if we adjusted internal state from the PreFilterMessage() function it would still not get displayed until Sampler() was called.."

    Caching some state telling the Sampler() method that a key was pressed and it should act on that doesn't seem to be too difficult.

  12. The problem I had before was that the Sampler() method only gets called in response to Windows messages coming into AutoCAD's message loop.

    Obviously the keyboard being used qualifies - and should cause Sampler() to be called - so perhaps having PreFilterMessage() not filter out the message (returning false) would cause AutoCAD to also "get the message". Although depending on the key being used, AutoCAD may choose to interpret that in its own way.

    I've mostly just convinced myself that I'm still mainly in favour of the current implementation, overall. 🙂

    Kean

  13. If I do not want a fixed text height what I need to change because
    Thank you

    Wenn ich keine feste Texthöhe will was muss ich da ändern
    Danke

  14. You could ask the user for the size before the text string (using Editor.GetDouble()) and then pass that as a parameter into the jig.

    Or you could add a keyword that caused the user to be asked to enter the size (again, a double) during the jig itself.

    Kean

  15. You could ask the user for the size before the text string (using Editor.GetDouble()) and then pass that as a parameter into the jig.

    Where would that be exactly I'm beginner

    Thank you

    Wo wäre das genau ich bin Anfänger

    Danke

  16. You should be able to find lots of examples of using GetDouble() on this blog. Copy & paste some code that does so.

    Then modify the constructor as follows:

    public TextPlacementJig(
    Transaction tr, Database db, Entity ent,
    double txtSize
    ) : base(ent)
    {
    _db = db;
    _tr = tr;
    _angle = 0;
    _txtSize = txtSize;
    }

    Then you can simply make sure you pass the results of the GetDouble() call to the call to create the jig:

    TextPlacementJig pj =
    new TextPlacementJig(tr, db, txt, pdr.Result);

    I hope this helps,

    Kean

  17. Get back the

    Fehler1Der Name "pdr" ist im aktuellen Kontext nicht vorhanden.

    Danke

  18. Unless you copied & pasted some code from another blog post (as I had suggested), then yes, you would get this error. I generally use a variable named pdr to contain the results of the GetDouble() operation, as you can see in this - and numerous other - posts:

    keanw.com/2009/03/free-form-modeling-in-autocad-2010-using-net.html

    Here's the code from that one:

    PromptDoubleOptions pdo =
    new PromptDoubleOptions(
    "
    Specify bump size as fraction of bounds: "
    );
    PromptDoubleResult pdr =
    doc.Editor.GetDouble(pdo);

    You would change the string to say something like "
    Enter text size: ", of course.

    Kean

  19. Thanks for the answer

    Should I again I anschaunen not get through this.

    Danke für die Antwort

    Muss ich mir nochmal anschaunen ich bekomme das nicht hin.

  20. Brilliant little program and very useful in everyday life.

    Is there a chance that you could show something similar for inserting dynamic blocks with multiple insertion points?

    When inserting them via INSERT, one can toggle between the different insertion points via CTRL but I can't find any way of achieving this behavious within a jig.

    It would be much appreciated if you could only give a hint where to look because I am trying since ages and can't get it running.

    Thanks

  21. It should be possible (I'd probably start with the jig in this post: keanw.com/2007/05/using_a_jig_fro_1.html).

    The main issue is likely to be getting the insertion point data from the dynamic block definition (again, this may help: keanw.com/2009/03/accessing-the-properties-of-a-dynamic-autocad-block-using-net.html).

    You'd then need to decide which point you were using (controlled by some internal state which changes based on the keys pressed, as in this post), and adjust the current insertion point relative to that.

    Hopefully this is enough to get you started, at least...

    Kean

  22. Thanks for the trick of the '_' in the sendcomand.

    I used the IMessageFilter after my previous comment (TEXT part 3)

    I check for the Key '+' and key '-' but the SetMessageAndKeywords dont want it. so with your trick i can send "Larger" with my key '+' Good !!

    I have tryed to detecte the mouseclick right.
    It works fine but when i click out the limit of the draw the command go nuts. I dont know why. So i use the key 'r' for initiate the rotation

    Thx
    Very good example.

  23. Hi Kean

    Thank you kindly and if my understanding is correct, it would turn out to be just a transformation on the block-jig rather than fiddeling around with grip-points etc.

    I have probably started from the wrong end then and will try this approach instead.

    Cheers

  24. Hi Toby,

    That's right - you can just translate the block's insertion point by the vectors defining the various insertion points in the block definition.

    Should be straightforward, I'd hope.

    Kean

Leave a Reply to Tony Tanzillo Cancel reply

Your email address will not be published. Required fields are marked *