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.
You might also like: