In the last post, we saw a great little sample for adding a textbox to AutoCAD’s ribbon which notifies your application of the “commands” entered into it (however you choose to interpret them in your code).
In this post, we’ll take that further and have that textbox expand vertically as text gets entered, wrapping the contents across multiple lines (only breaking the text at the ends of words, too).
Here’s the updated C# code, with modified lines in red. You can get the original file here.
1 using Autodesk.AutoCAD.ApplicationServices;
2 using Autodesk.AutoCAD.EditorInput;
3 using Autodesk.AutoCAD.Runtime;
4 using Autodesk.Windows;
5 using System.Windows.Media;
6 using System.Windows.Controls;
7 using System.Windows.Input;
8 using System.Windows;
9 using System;
10
11 namespace NotifyingRibbonTextbox
12 {
13 public class Commands
14 {
15 public bool _added = false;
16
17 [CommandMethod("RTB")]
18 public void RibbonTextBox()
19 {
20 if (!_added)
21 {
22 // Look for the standard "Plug-ins" tab
23
24 RibbonControl rc = ComponentManager.Ribbon;
25 RibbonTab rt = null;
26
27 foreach (RibbonTab tab in rc.Tabs)
28 {
29 if (tab.AutomationName == "Plug-ins")
30 {
31 rt = tab;
32 break;
33 }
34 }
35
36 // If we didn't find it, create a custom tab
37
38 if (rt == null)
39 {
40 rt = new RibbonTab();
41 rt.Title = "Custom";
42 rt.Id = "ID_CUSTOMRIBBONTAB";
43 rc.Tabs.Add(rt);
44 }
45
46 // Create our custom panel, add it to the ribbon tab
47
48 RibbonPanelSource rps = new RibbonPanelSource();
49 rps.Title = "Notifying Textbox";
50 RibbonPanel rp = new RibbonPanel();
51 rp.Source = rps;
52 rt.Panels.Add(rp);
53
54 // Create our custom textbox, add it to the panel
55
56 NotifyingTextBox tb =
57 new NotifyingTextBox(150, 15, 17, 5);
58 tb.IsEmptyTextValid = false;
59 tb.AcceptTextOnLostFocus = true;
60 tb.InvokesCommand = true;
61 tb.CommandHandler = new TextboxCommandHandler();
62 rps.Items.Add(tb);
63
64 // Set our tab to be active
65
66 rt.IsActive = true;
67
68 // We only want to add it once, so set a flag
69
70 _added = true;
71 }
72 }
73
74 public static void Print(string s)
75 {
76 // A simple helper to write to the command-line
77
78 Document doc =
79 Autodesk.AutoCAD.ApplicationServices.
80 Application.DocumentManager.MdiActiveDocument;
81 doc.Editor.WriteMessage(s);
82 }
83 }
84
85 public class TextboxCommandHandler : ICommand
86 {
87 #pragma warning disable 67
88 public event EventHandler CanExecuteChanged;
89 #pragma warning restore 67
90
91 public bool CanExecute(object parameter)
92 {
93 // Yes, we can execute
94
95 return true;
96 }
97
98 public void Execute(object parameter)
99 {
100 // Dump the textbox contents to the command-line
101
102 NotifyingTextBox tb = parameter as NotifyingTextBox;
103 if (tb != null)
104 {
105 Commands.Print(
106 "\nRibbon Textbox: " +
107 tb.GetTextWithoutNewlines() + "\n"
108 );
109 tb.ClearText();
110 }
111 }
112 }
113
114 public class NotifyingTextBox : RibbonTextBox
115 {
116 double _baseHeight;
117 double _baseWidth;
118 double _heightPadding;
119 double _widthPadding;
120 bool _textChanging = false;
121
122 public NotifyingTextBox(
123 double width, double height,
124 double widthPadding,
125 double heightPadding
126 )
127 {
128 // Set some member variables, some of which
129 // we also use to set the TextBox dimensions
130
131 _baseWidth = width;
132 _baseHeight = height;
133 _widthPadding = widthPadding;
134 _heightPadding = heightPadding;
135
136 Width = width;
137 Height = height;
138 MinWidth = width;
139
140 // Register our focus-related event handlers
141
142 EventManager.RegisterClassHandler(
143 typeof(TextBox), TextBox.GotKeyboardFocusEvent,
144 new RoutedEventHandler(OnGotFocus)
145 );
146
147 EventManager.RegisterClassHandler(
148 typeof(TextBox), TextBox.LostKeyboardFocusEvent,
149 new RoutedEventHandler(OnLostFocus)
150 );
151
152 // And out additional TextChanged event handler
153
154 EventManager.RegisterClassHandler(
155 typeof(TextBox), TextBox.TextChangedEvent,
156 new RoutedEventHandler(OnTextChanged)
157 );
158 }
159
160 public string GetTextWithoutNewlines()
161 {
162 // Return the contained text without newline characters
163
164 return TextValue.ReplaceNewlinesWithSpaces();
165 }
166
167 public void ClearText()
168 {
169 TextValue = "";
170 }
171
172 private void OnTextChanged(
173 object sender, RoutedEventArgs e
174 )
175 {
176 if (!_textChanging && e != null && e.Source != null)
177 {
178 TextBox tb = e.Source as TextBox;
179
180 if (tb != null)
181 {
182 // We need the typeface to calculate the text width
183
184 var faces = tb.FontFamily.GetTypefaces();
185 Typeface face = null;
186 foreach (Typeface tf in faces)
187 {
188 if (tf != null)
189 {
190 face = tf;
191 break;
192 }
193 }
194
195 // Get the last line of text, to see how long it is
196
197 string text = tb.Text.GetLastLine();
198
199 // Calculate the width of this last line of text
200
201 FormattedText ft =
202 new FormattedText(
203 text,
204 System.Globalization.CultureInfo.CurrentCulture,
205 FlowDirection.LeftToRight,
206 face,
207 tb.FontSize * (96.0 / 72.0),
208 Brushes.Black
209 );
210
211 // If the width of the last line of text
212 // is over our boundary (the width of the box
213 // minus some padding), then start the next
214 // line
215
216 if (ft.Width - _widthPadding > _baseWidth)
217 {
218 // Set our flag to stop re-entry of the event
219 // handler, then replace the last space in the
220 // TextBox contents with a newline
221
222 _textChanging = true;
223 tb.Text =
224 tb.Text.InsertNewlineAtLastSpace();
225 _textChanging = false;
226
227 // Set the cursor to be at the end of our text
228 // so that typing continues properly
229
230 tb.SelectionStart = tb.Text.Length;
231 }
232
233 // Find the number of lines of text
234
235 int lines = tb.Text.GetLineCount();
236
237 // Change the height based on the number of lines
238
239 tb.Height = _heightPadding + (lines * _baseHeight);
240 tb.MinLines = lines;
241 }
242 }
243 }
244
245 // Both events call the same helper, with a custom message
246
247 private void OnGotFocus(object sender, RoutedEventArgs e)
248 {
249 OnFocusChange(sender, e, "\nTextbox got focus :)\n");
250 }
251
252 private void OnLostFocus(object sender, RoutedEventArgs e)
253 {
254 OnFocusChange(sender, e, "\nTextbox lost focus :(\n");
255 }
256
257 // Our helper function to print a message only when
258 // our custom textbox exists
259
260 private void OnFocusChange(
261 object sender, RoutedEventArgs e, string msg
262 )
263 {
264 if (e != null && e.Source != null)
265 {
266 TextBox tb = e.Source as TextBox;
267
268 if (tb != null)
269 {
270 NotifyingTextBox mtb =
271 tb.DataContext as NotifyingTextBox;
272
273 if (mtb != null)
274 {
275 Commands.Print(msg);
276 }
277 }
278 }
279 }
280 }
281
282 // String-related extension methods
283
284 public static class StringExtensions
285 {
286 const string newline = "\r\n";
287
288 public static string InsertNewlineAtLastSpace(
289 this string s
290 )
291 {
292 // If the string contains a space, replace it
293 // with a newline character, otherwise we simply
294 // append the newline to the string
295
296 string ret;
297 if (s.Contains(" "))
298 {
299 int index = s.LastIndexOf(" ");
300 ret =
301 s.Substring(0, index) + newline +
302 s.Substring(index + 1);
303 }
304 else
305 {
306 ret = s + newline;
307 }
308 return ret;
309 }
310
311 public static string ReplaceNewlinesWithSpaces(
312 this string s
313 )
314 {
315 // Replace all the newlines with spaces
316
317 if (String.IsNullOrEmpty(s))
318 return s;
319 return s.Replace(newline, " ");
320 }
321
322 public static string GetLastLine(this string s)
323 {
324 // Return the last line of the text (or
325 // the whole thing if there's no newline in it)
326
327 string ret;
328 if (s.Contains(newline))
329 {
330 ret =
331 s.Substring(
332 s.LastIndexOf(newline) + newline.Length
333 );
334 }
335 else
336 {
337 ret = s;
338 }
339 return ret;
340 }
341
342 public static int GetLineCount(this string s)
343 {
344 // Count the number of lines by checking the
345 // overall length of the string against the
346 // string without newline sequences in it
347
348 return
349 (
350 (s.Length -
351 s.Replace(newline, "").Length)
352 / newline.Length
353 ) + 1;
354 }
355 }
356 }
In terms of changes to the code, the main things to note are some additional parameters on the constructor of the NotifyingTextBox class (which control the size and behaviour of the textbox), where we also attach another event handler to be called when the contents of the textbox are modified.
In this event handler, we calculate the width of the last line of text (basically the one being typed), to see whether we need to break the line or not. We use the FormattedText class to do this, which uses the font and its size from the textbox (with some modification to convert the size, as per this information). If the line is wider than the width of our textbox – taking into account an additional buffer amount, just to make it work better – we insert a newline at the preceding space, which keeps the entered words intact.
Something to note: we check a flag in the event handler to stop re-entering when we modify the text by inserting the newline. If we didn’t do this we’d soon get ourselves in trouble (trust me on this ;-).
Otherwise, the code itself should be reasonably straightforward. It uses some extension methods on the string class to make life a little easier, but that's about it.
Putting this updated code through its paces…
Now when we run the RTB command and start to enter text into our textbox, we see it expands as we get to the end of a line:
And it keeps on going:
When we’ve finished, it echoes the contents – without the additional line-breaks – to the command-line:
Textbox got focus :)
Ribbon Textbox: Let's now enter a longer string of text, to see what
happens when we write and write and write and write and write - we
actually see the ribbon starting to stretch, which is pretty cool.
Textbox lost focus :(
I didn’t manage to get the ribbon to stretch horizontally: the width seems based on the size of the controls you place on it at startup, so while we can make the textbox stretch horizontally, getting the ribbon panel to accommodate the larger size proved harder (i.e. I couldn’t work out how to do it). Please post a comment if you’ve managed to do this, yourself.