« May 2007 | Main | July 2007 »
Using a modeless .NET dialog to display AutoCAD object properties
In this previous post we looked at creating a simple modal dialog and using it to display object properties. This post looks at the structural changes you need to make to your application for the same dialog to be used modelessly. In a later post we'll look at the benefits you get from leveraging the Palette system for modeless interaction inside AutoCAD.
Firstly, let's think about the interaction paradigm needed by a modeless dialog. A few things come to mind:
- There is no longer a need to hide and show the dialog around selection
- Rather than asking the user to select an entity, it's neater to respond to standard selection events
- We no longer need a "browse" button
- We now need to be more careful about document access
- Our command automatically locked the document (and had sole access) in the modal example
- We should now lock it "manually" when we access it
So we can already simplify our dialog class - here's some modified C# code, with the browse button removed and document-locking in place:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace CustomDialogs
{
public partial class TypeViewerForm : Form
{
public TypeViewerForm()
{
InitializeComponent();
}
public void SetObjectText(string text)
{
typeTextBox.Text = text;
}
public void SetObjectId(ObjectId id)
{
if (id == ObjectId.Null)
{
SetObjectText("");
}
else
{
Document doc =
Autodesk.AutoCAD.ApplicationServices.
Application.DocumentManager.MdiActiveDocument;
DocumentLock loc =
doc.LockDocument();
using (loc)
{
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
DBObject obj = tr.GetObject(id, OpenMode.ForRead);
SetObjectText(obj.GetType().ToString());
tr.Commit();
}
}
}
}
private void closeButton_Click(object sender, EventArgs e)
{
this.Close();
}
}
}
So which event should we respond to, to find out when objects are selected? In this case I chose a PointMonitor - this class tells you a lot of really useful information about the current selection process. It also has the advantage of picking up the act of hovering over objects - no need for selection to actually happen. One other fun option would have been to use a Database event (ObjectAppended) to display information about objects as they are added to the drawing.
A few other comments about the code:
- Predictably enough we now use ShowModelessDialog rather than ShowModalDialog()
- We have our form as a member variable of the class, as its lifespan goes beyond the command we use to show it
- I've also removed the selection code; we're no longer asking for objects to be selected
Here's the updated command implementation:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using CustomDialogs;
namespace CustomDialogs
{
public class Commands
{
TypeViewerForm tvf;
public Commands()
{
tvf = new TypeViewerForm();
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ed.PointMonitor +=
new PointMonitorEventHandler(OnMonitorPoint);
}
~Commands()
{
try
{
tvf.Dispose();
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ed.PointMonitor -=
new PointMonitorEventHandler(OnMonitorPoint);
}
catch(System.Exception)
{
// The editor may no longer
// be available on unload
}
}
private void OnMonitorPoint(
object sender,
PointMonitorEventArgs e
)
{
FullSubentityPath[] paths =
e.Context.GetPickedEntities();
if (paths.Length <= 0)
{
tvf.SetObjectId(ObjectId.Null);
return;
};
ObjectId[] objs = paths[0].GetObjectIds();
if (objs.Length <= 0)
{
tvf.SetObjectId(ObjectId.Null);
return;
};
// Set the "selected" object to be the last in the list
tvf.SetObjectId(objs[objs.Length - 1]);
}
[CommandMethod("vt",CommandFlags.UsePickSet)]
public void ViewType()
{
Application.ShowModelessDialog(null, tvf, false);
}
}
}
And here's the source project for this version of the application. When you run the application you may experience issues with the dialog getting/retaining focus - this is generally a problem with modeless dialogs that has been addressed automatically by the Palette class, something we'll take a look at in a future post.
June 29, 2007 in AutoCAD, AutoCAD .NET, Object properties, User interface | Permalink | Comments (9) | TrackBack
Using a modal .NET dialog to display AutoCAD object properties
Firstly, a big thanks for all your comments on the first anniversary post. It's good to know that people are finding this blog useful, and I hope the flow of ideas (internal and external) doesn't dry up anytime soon. So keep the comments coming! :-)
This post is going to start a sequence of posts that look at how to integrate forms into AutoCAD. This post looks at modal forms, and later on we'll look more at modeless forms and - in particular - palettes.
Just to be clear, these posts will focus on the basic integration - the more advanced activity of detailed property display (etc.) are left as an exercise for the reader. For example, in today's code we're simply going to get the type of an object and put that text into our dialog. Nothing very complex, but it shows the basic interaction between AutoCAD objects and WinForms.
Before we get started, I should very quickly define "modal" and "modeless", for those that aren't familiar with the terminology. Modal dialogs take exclusive control of an application's user-input functions, while modeless dialogs can co-exist with other modeless dialogs and user-input handling. Which means that the two types of dialog have different issues to deal with (in terms of how the assumptions they make on accessing data etc.).
So next we need to create our form... I'm not going to step through the process here - we'll focus on the code - so to make life easier I've packaged up the source here.
Here's the C# code for the command class. It's very simple: it checks the pickfirst selection and uses the first object as input for the form (assigning the object ID to the form, which will then go away and retrieve the object's type). At this stage we're just supporting one object - we're not going through the effort of determining shared properties across objects etc. We then use Application.ShowModalDialog() to show the form inside AutoCAD.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using CustomDialogs;
namespace CustomDialogs
{
public class Commands
{
[CommandMethod("vt",CommandFlags.UsePickSet)]
public void ViewType()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
TypeViewerForm tvf = new TypeViewerForm();
PromptSelectionResult psr =
ed.GetSelection();
if (psr.Value.Count > 0)
{
ObjectId selId = psr.Value[0].ObjectId;
tvf.SetObjectId(selId);
}
if (psr.Value.Count > 1)
{
ed.WriteMessage(
"\nMore than one object was selected: only using the first.\n"
);
}
Application.ShowModalDialog(null, tvf, false);
}
}
}
The form itself is a little more complex (but barely). It contains a function that can be used to set the active object (by its ID, as mentioned above) which goes away and opens the object, getting its type. The form also contains a "browse" button, which can be used to change the actively selected object.
Here's the C# code for the form:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace CustomDialogs
{
public partial class TypeViewerForm : Form
{
public TypeViewerForm()
{
InitializeComponent();
}
public void SetObjectText(string text)
{
typeTextBox.Text = text;
}
public void SetObjectId(ObjectId id)
{
if (id == ObjectId.Null)
{
SetObjectText("");
}
else
{
Document doc =
Autodesk.AutoCAD.ApplicationServices.
Application.DocumentManager.MdiActiveDocument;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
DBObject obj = tr.GetObject(id, OpenMode.ForRead);
SetObjectText(obj.GetType().ToString());
tr.Commit();
}
}
}
private void closeButton_Click(object sender, EventArgs e)
{
this.Close();
}
private void browseButton_Click(object sender, EventArgs e)
{
DocumentCollection dm =
Autodesk.AutoCAD.ApplicationServices.
Application.DocumentManager;
Editor ed =
dm.MdiActiveDocument.Editor;
Hide();
PromptEntityResult per =
ed.GetEntity("\nSelect entity: ");
if (per.Status == PromptStatus.OK)
{
SetObjectId(per.ObjectId);
}
else
{
SetObjectId(ObjectId.Null);
}
Show();
}
}
}
Here's what happens when you run the VT command and select a circle:
As an aside... I'm going on holiday on Wednesday for a week. We have our annual football (meaning soccer, of course) tournament being held this year in the UK. Teams from Autodesk offices from around the world (it used to have a European focus, but it's gained in popularity in the last few years) will fly in and take part. I'll be playing for one of the Swiss teams, and it'll certainly be lots of fun to catch up with old friends from other locations. The tournament is at the weekend, but I'm taking some days off either side to catch up with family and friends. I doubt I'll have time to queue up additional posts for while I'm away, so this is just to let you know that things may go a little quiet for the next week or so.
June 25, 2007 in AutoCAD, AutoCAD .NET, Object properties, User interface | Permalink | Comments (2) | TrackBack
One year wiser?
Well, would you believe it: Through the Interface was started exactly a year ago today.
I hope you've found this blog helpful over the past 12 months; I've certainly enjoyed sharing my (and above all, my team's) knowledge with you, and look forward to doing so for a long time to come. But please keep the post suggestions coming, as many of the best posts over the last year (in my opinion, at least) have originated from suggestions from this blog's readership.
By the way, for those of you who are interested: I'm now back from Beijing - we arrived in Switzerland a little over 2 weeks ago, having stopped by Bangalore for 2 weeks on the way home to spend time with the DevTech team there and catch up with friends and relatives. As you can imagine, 2+ months away (7 weeks in China, 2 weeks in India) was quite tough, even if we were all together as a family. We very often missed our home comforts, it has to be said, but the experience was certainly valuable.
Two days after getting back from the trip to China & India, I had to head across to Munich for some internal meetings. This afternoon I'm heading to Boston for some more. But all being well I should have another technical post for you by the end of the week.
June 19, 2007 in Personal | Permalink | Comments (10) | TrackBack
Creating a table of block attributes in AutoCAD using .NET - Part 2
In the last post we looked at some code to create a table of attribute values for a particular block. In this post we'll extend that code and show how to use a formula to create a total of those values.
Below is the C# code. I've numbered the lines, and those in red are new since the last post. The complete source file can be downloaded here.
Firstly, a quick breakdown of the changes:
- Lines 60-81 deal with user input, and the forcing of the decision to "embed" rather than "link", if we're performing the total (table formulae do not work with fields, even if they have numeric results, so we're forced to create the table with the current value, rather than a field pointing to the attribute reference)
- Line 134 and subsequently lines 159-166 declare and set a variable indicating for which column we're going to provide a total
- Lines 169-181 deal with the exceptional case that we don't find the specified attribute definition
- Lines 310-336 create our additional row, and insert the total in the appropriate cell. We're using a formula such as this: %<\AcExpr (Sum(A2:A4)) \f "%lu2%pr2">%
- The \f flag specifies we want a numeric value with 2 decimal places - these codes are not documented, but you can find them out by using the FIELD command, as described in this previous post
- Line 343 performs a regen, to update the value of our field
And now for the code:
1 using Autodesk.AutoCAD.ApplicationServices;
2 using Autodesk.AutoCAD.DatabaseServices;
3 using Autodesk.AutoCAD.EditorInput;
4 using Autodesk.AutoCAD.Geometry;
5 using Autodesk.AutoCAD.Runtime;
6 using System.Collections.Specialized;
7 using System;
8
9 namespace TableCreation
10 {
11 public class Commands
12 {
13 // Set up some formatting constants
14 // for the table
15
16 const double colWidth = 15.0;
17 const double rowHeight = 3.0;
18 const double textHeight = 1.0;
19 const CellAlignment cellAlign =
20 CellAlignment.MiddleCenter;
21
22 // Helper function to set text height
23 // and alignment of specific cells,
24 // as well as inserting the text
25
26 static public void SetCellText(
27 Table tb,
28 int row,
29 int col,
30 string value
31 )
32 {
33 tb.SetAlignment(row, col, cellAlign);
34 tb.SetTextHeight(row, col, textHeight);
35 tb.SetTextString(row, col, value);
36 }
37
38 [CommandMethod("BAT")]
39 static public void BlockAttributeTable()
40 {
41 Document doc =
42 Application.DocumentManager.MdiActiveDocument;
43 Database db = doc.Database;
44 Editor ed = doc.Editor;
45
46 // Ask for the name of the block to find
47
48 PromptStringOptions opt =
49 new PromptStringOptions(
50 "\nEnter name of block to list: "
51 );
52 PromptResult pr = ed.GetString(opt);
53
54 if (pr.Status == PromptStatus.OK)
55 {
56 string blockToFind =
57 pr.StringResult.ToUpper();
58 bool embed = false;
59
60 // And the attribute to provide total for
61
62 opt.Message =
63 "\nEnter name of column to total <\"\">: ";
64 pr = ed.GetString(opt);
65
66 if (pr.Status == PromptStatus.None ||
67 pr.Status == PromptStatus.OK)
68 {
69 string columnToTotal =
70 pr.StringResult.ToUpper();
71
72 if (columnToTotal != "")
73 {
74 // If a column has been chosen, we need
75 // to embed the attribute values
76 // as otherwise the "sum" formula will fail
77
78 embed = true;
79 }
80 else
81 {
82 // Ask whether to embed or link
83
84 PromptKeywordOptions pko =
85 new PromptKeywordOptions(
86 "\nEmbed or link the attribute values: "
87 );
88
89 pko.AllowNone = true;
90 pko.Keywords.Add("Embed");
91 pko.Keywords.Add("Link");
92 pko.Keywords.Default = "Embed";
93 PromptResult pkr =
94 ed.GetKeywords(pko);
95
96 if (pkr.Status == PromptStatus.None ||
97 pkr.Status == PromptStatus.OK)
98 {
99 if (pkr.Status == PromptStatus.None ||
100 pkr.StringResult == "Embed")
101 embed = true;
102 else
103 embed = false;
104 }
105 }
106
107 Transaction tr =
108 doc.TransactionManager.StartTransaction();
109 using (tr)
110 {
111 // Let's check the block exists
112
113 BlockTable bt =
114 (BlockTable)tr.GetObject(
115 doc.Database.BlockTableId,
116 OpenMode.ForRead
117 );
118
119 if (!bt.Has(blockToFind))
120 {
121 ed.WriteMessage(
122 "\nBlock "
123 + blockToFind
124 + " does not exist."
125 );
126 }
127 else
128 {
129 // And go through looking for
130 // attribute definitions
131
132 StringCollection colNames =
133 new StringCollection();
134 int colToTotalIdx = -1;
135
136 BlockTableRecord bd =
137 (BlockTableRecord)tr.GetObject(
138 bt[blockToFind],
139 OpenMode.ForRead
140 );
141 foreach (ObjectId adId in bd)
142 {
143 DBObject adObj =
144 tr.GetObject(
145 adId,
146 OpenMode.ForRead
147 );
148
149 // For each attribute definition we find...
150
151 AttributeDefinition ad =
152 adObj as AttributeDefinition;
153 if (ad != null)
154 {
155 // ... we add its name to the list
156
157 colNames.Add(ad.Tag);
158
159 if (ad.Tag.ToUpper() == columnToTotal)
160 {
161 // Save the index of the column
162 // we want to total
163
164 colToTotalIdx =
165 colNames.Count - 1;
166 }
167 }
168 }
169 // If we didn't find the attribute to be totalled
170 // then simply ignore the request and continue
171
172 if (columnToTotal != "" && colToTotalIdx < 0)
173 {
174 ed.WriteMessage(
175 "\nAttribute definition for "
176 + columnToTotal
177 + " not found in "
178 + blockToFind
179 + ". Total will not be added to the table."
180 );
181 }
182 if (colNames.Count == 0)
183 {
184 ed.WriteMessage(
185 "\nThe block "
186 + blockToFind
187 + " contains no attribute definitions."
188 );
189 }
190 else
191 {
192 // Ask the user for the insertion point
193 // and then create the table
194
195 PromptPointResult ppr =
196 ed.GetPoint(
197 "\nEnter table insertion point: "
198 );
199
200 if (ppr.Status == PromptStatus.OK)
201 {
202 Table tb = new Table();
203 tb.TableStyle = db.Tablestyle;
204 tb.NumRows = 1;
205 tb.NumColumns = colNames.Count;
206 tb.SetRowHeight(rowHeight);
207 tb.SetColumnWidth(colWidth);
208 tb.Position = ppr.Value;
209
210 // Let's add our column headings
211
212 for (int i = 0; i < colNames.Count; i++)
213 {
214 SetCellText(tb, 0, i, colNames[i]);
215 }
216
217 // Now let's search for instances of
218 // our block in the modelspace
219
220 BlockTableRecord ms =
221 (BlockTableRecord)tr.GetObject(
222 bt[BlockTableRecord.ModelSpace],
223 OpenMode.ForRead
224 );
225
226 int rowNum = 1;
227 foreach (ObjectId objId in ms)
228 {
229 DBObject obj =
230 tr.GetObject(
231 objId,
232 OpenMode.ForRead
233 );
234 BlockReference br =
235 obj as BlockReference;
236 if (br != null)
237 {
238 BlockTableRecord btr =
239 (BlockTableRecord)tr.GetObject(
240 br.BlockTableRecord,
241 OpenMode.ForRead
242 );
243 using (btr)
244 {
245 if (btr.Name.ToUpper() == blockToFind)
246 {
247 // We have found one of our blocks,
248 // so add a row for it in the table
249
250 tb.InsertRows(
251 rowNum,
252 rowHeight,
253 1
254 );
255
256 // Assume that the attribute refs
257 // follow the same order as the
258 // attribute defs in the block
259
260 int attNum = 0;
261 foreach (
262 ObjectId arId in
263 br.AttributeCollection
264 )
265 {
266 DBObject arObj =
267 tr.GetObject(
268 arId,
269 OpenMode.ForRead
270 );
271 AttributeReference ar =
272 arObj as AttributeReference;
273 if (ar != null)
274 {
275 // Embed or link the values
276
277 string strCell;
278 if (embed)
279 {
280 strCell = ar.TextString;
281 }
282 else
283 {
284 string strArId =
285 arId.ToString();
286 strArId =
287 strArId.Trim(
288 new char[] { '(', ')' }
289 );
290 strCell =
291 "%<\\AcObjProp Object("
292 + "%<\\_ObjId "
293 + strArId
294 + ">%).TextString>%";
295 }
296 SetCellText(
297 tb,
298 rowNum,
299 attNum,
300 strCell
301 );
302 }
303 attNum++;
304 }
305 rowNum++;
306 }
307 }
308 }
309 }
310
311 // Now let's add a row for our total
312
313 if (colToTotalIdx >= 0)
314 {
315 tb.InsertRows(rowNum, rowHeight, 1);
316 char colLetter =
317 Convert.ToChar(
318 (Convert.ToInt32(
319 'A') + colToTotalIdx
320 )
321 );
322
323 // Add a formula to sum the column
324
325 SetCellText(
326 tb,
327 rowNum,
328 colToTotalIdx,
329 "%<\\AcExpr (Sum("
330 + colLetter
331 + "2:"
332 + colLetter
333 + rowNum.ToString()
334 + ")) \\f \"%lu2%pr2\">%"
335 );
336 }
337 tb.GenerateLayout();
338
339 ms.UpgradeOpen();
340 ms.AppendEntity(tb);
341 tr.AddNewlyCreatedDBObject(tb, true);
342 tr.Commit();
343 ed.Regen();

Atom
