Adding a custom tab to AutoCAD's options dialog using .NET - Part 2
This post extends the approach shown in this previous post to implement a realistic editing and storage mechanism for application settings. It uses the .NET PropertyGrid control to display a custom class, allowing editing of a number of properties. This class is also serializable, which means we can use the .NET Framework to save it out to an XML file on disk. Some readers may have their own approaches to saving custom application settings, whether in the Registry or elsewhere: this post is primarily about displaying properties rather than providing a definitive "how to" for storing custom application settings. I chose a path of relatively low resistance, which will hopefuly prove interesting to some of the people reading the post.
MSDN contains a useful page on implementing the PropertyGrid in your project, but there are lots of other helpful pages you'll find on The Code Project and other sites.
Here's a project containing the code from this post, in case you'd prefer not to create it yourself.
As in the first part of the series, we need to add a User Control to our project. Within this control we'll add a single PropertyGrid, drawn to the full extents of the control (I found that drawing it to fill the container and then setting "Anchor" to "Top, Bottom, Left, Right" worked better than setting "Dock" to "Fill"). Thinking about it, it would probably work just to create a PropertyGrid in code and pass that into the constructor of the TabbedDialogExtension object, but doing it this way allows us to make use of the designer to play around with the control's properties at design-time, rather than making the settings dynamically at runtime.
Here's an idea of what the design should look like of our user control containing the property grid (nothing very impressive or exciting, at this stage - I'm basically just including it for completeness :-):
I customized the layout of the PropertyGrid somewhat - modifying the font and the background colour of the categories - but you will see that from the below snapshots or from the sample project.
Next we need to add some code. Here's the code behind this control, where we use the "value changed" event to signal that our tab's data is "dirty" and may require saving:
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
namespace OptionsDlg
{
public partial class OptionsTabControl : UserControl
{
public OptionsTabControl()
{
InitializeComponent();
}
private void propertyGrid_PropertyValueChanged(
object sender,
System.Windows.Forms.PropertyValueChangedEventArgs e
)
{
TabbedDialogExtension.SetDirty(this, true);
}
}
}
Here's the code for the rest of our application's implementation (stored in a separate .cs file - I called mine Application.cs, although you might prefer to split it into AppSettings.cs and Initialization.cs):
using System;
using System.Web.UI;
using System.IO;
using System.Xml.Serialization;
using System.ComponentModel;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
[assembly:
ExtensionApplication(
typeof(OneNeedsOptions.Initialization)
)
]
namespace OneNeedsOptions
{
public enum Fruit
{
Orange,
Banana,
Strawberry,
Apple
}
[Serializable(),
DefaultProperty("Name")
]
public class AppSettings
{
// Our internal properties
private string _name = "Kean Walmsley";
private string _url =
"http://blogs.autodesk.com/through-the-interface";
private DateTime _birthday = new DateTime(1912, 7, 14);
private Fruit _fruit = Fruit.Strawberry;
// Their external exposure and categorization/description
[Description("The person's name"),
Category("Identity")
]
public string Name
{
set { _name = value; }
get { return _name; }
}
[Description("The blog written by this person"),
Category("Stuff I do"),
UrlProperty()
]
public string Blog
{
set { _url = value; }
get { return _url; }
}
[Description("The day this person was born"),
Category("Identity")
]
public DateTime Birthday
{
set { _birthday = value; }
get { return _birthday; }
}
[Description("The person's age"),
Category("Identity"),
ReadOnly(true)
]
public int Age
{
get
{
return
(int)((DateTime.Now - _birthday).Days / 365.25);
}
}
[Description("The person's favourite fruit"),
Category("Stuff I like")
]
public Fruit FavouriteFruit
{
set { _fruit = value; }
get { return _fruit; }
}
const string filename = "AppSettings.xml";
// Our methods for loading and saving the settings
// Load needs to be static, as we don't yet have
// an instance
public static AppSettings Load()
{
AppSettings ret = null;
XmlSerializer xs = null;
StreamReader sr = null;
try
{
xs = new XmlSerializer(typeof(AppSettings));
sr = new StreamReader(filename);
}
catch
{
// File not found: create default settings
return new AppSettings();
}
if (sr != null)
{
ret = (AppSettings)xs.Deserialize(sr);
sr.Close();
}
return ret;
}
// Save will be called on a specific instance
public void Save()
{
try
{
XmlSerializer xs =
new XmlSerializer(typeof(AppSettings));
StreamWriter sw =
new StreamWriter(filename, false);
xs.Serialize(sw, this);
sw.Close();
}
catch (System.Exception ex)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(
"\nUnable to save the application settings: {0}",
ex
);
}
}
}
class Initialization : IExtensionApplication
{
static AppSettings _settings = null;
public void Initialize()
{
Application.DisplayingOptionDialog +=
new TabbedDialogEventHandler(
Application_DisplayingOptionDialog
);
}
public void Terminate()
{
Application.DisplayingOptionDialog -=
new TabbedDialogEventHandler(
Application_DisplayingOptionDialog
);
}
private static void OnOK()
{
_settings.Save();
}
private static void OnCancel()
{
_settings = AppSettings.Load();
}
private static void OnHelp()
{
// Not currently doing anything here
}
private static void OnApply()
{
_settings.Save();
}
private static void Application_DisplayingOptionDialog(
object sender,
TabbedDialogEventArgs e
)
{
if (_settings == null)
_settings = AppSettings.Load();
if (_settings != null)
{
OptionsDlg.OptionsTabControl otc =
new OptionsDlg.OptionsTabControl();
otc.propertyGrid.SelectedObject = _settings;
otc.propertyGrid.Update();
TabbedDialogExtension tde =
new TabbedDialogExtension(
otc,
new TabbedDialogAction(OnOK),
new TabbedDialogAction(OnCancel),
new TabbedDialogAction(OnHelp),
new TabbedDialogAction(OnApply)
);
e.AddTab("My Application Settings", tde);
}
}
}
}
The interesting stuff is in the AppSettings class: it defines a number of properties (for which I've set default values as they're declared - you could also put them in a constructor, should you so wish), which are then exposed externally. It's these public properties that are interesting, as we've used attributes to indicate how the properties should be categorized, described and whether they're editable. The rest of the class contains the protocol to load and save the settings: we use the .NET Framework to do the heavy lifting of saving the contents to a file (which we've simply called AppSettings.xml, without specifying the location, which means it will be stored wherever your module is located), and loading them back in again.
Here's what the XML content looks like for the default settings, in case you're interested, although you should never really need to worry about it, unless you're interested in allowing more direct modification of the file contents:
<?xml version="1.0" encoding="utf-8" ?>
<AppSettings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Kean Walmsley</Name>
<Blog>http://blogs.autodesk.com/through-the-interface</Blog>
<Birthday>1912-07-14T00:00:00</Birthday>
<FavouriteFruit>Strawberry</FavouriteFruit>
</AppSettings>
A couple of more comments on the code...
We haven't bothered implementing the Help callback, but I've left it in their for your convenience (we could also have passed a null value into the construction of the TabbedDialogExtension object). From the callbacks for OK and Apply we call through to the AppSettings class to save the data; from Cancel we reload the last saved state, effectively cancelling any unsaved changes.
The AppSettings class will need to be accessible from elsewhere in your code (that's the point, really - settings aren't much use unless they're accessed), but I haven't actually shown this. It should simply be a matter of setting the _settings object to be public (or internal), or of exposing the data you care about via properties on the Initialization class.
Here's what tab looks like, once we've built the application, loaded it and launched the OPTIONS command inside AutoCAD:
As you edit the properties you'll see the controls available suit the property in question: there's a date picker for "Birthday" (which isn't actually my date of birth, by the way: it seems safer not to publish your birthday on the web, these days) and a combo-box for "FavouriteFruit". I wish there were better display of URLs in the grid, but that appears to be a standard complaint, and beyond the scope of this post. You will notice an "Age" property which has been made read-only as it's calculated from the date field. You'll also notice that the "Name" property is selected by default, because we indicated it as such using the DefaultProperty() attribute of the class.
八、AutoCad2010中新的Api February 13, 2009
The new APIs in AutoCAD 2010 - Part 1
This is the first post in a series looking at the new APIs in AutoCAD 2010, and follows on from this post looking at the migration steps required. I've copied the information in this post from the recently-published Platform Technologies Customization Newsletter, a quarterly newsletter available to ADN members. A big thank you to Stephen Preston, Fenton Webb and Gopinath Taget for putting the material together.
AutoCAD 2010 New API Overview
AutoCAD 2010 has some really cool APIs. Please download the ObjectARX 2010 Beta SDK and review the Migration Guide for a complete list of changes and additions.
Here are the highlights:
Overrule API
One of the most powerful ObjectARX APIs is the custom objects API. The custom object API allows you to create your own entities in a drawing that behave in the same way as standard AutoCAD entities. So, for example, where AutoCAD has a line, you might develop a custom entity that looks like a ‘pipe’. You can define how your pipe displays itself, the pipes grip- and snap- points, how the pipe behaves when moves or copied, etc.
However, with great power comes great responsibility. Custom objects are saved to a drawing. Without your Object Enabler, your custom object is loaded into AutoCAD as a dumb proxy object. So when you are considering creating a custom object, you need to consider whether you’re prepared to make a commitment to your application users that you will continue to support your custom object through multiple AutoCAD releases. If you’re not prepared to make that commitment, then you really shouldn’t be creating custom objects.
And because your custom object is responsible for filing itself when a drawing is saved or opened, you also have an extremely powerful mechanism for corrupting all your customers drawings if you make a mistake in your implementation.
To provide you with an alternative to custom objects – an alternative that requires less long term support commitment from you – AutoCAD 2010 introduces the new Overrule API. Think of Overrule as customized objects, rather than custom objects. It’s essentially a mechanism for AutoCAD to call your implementation of certain object functions instead of immediately calling the functions for that object. Your implementation can then choose whether to refer the call back to the native object. Unlike custom objects, the overrule definitions are not filed to the DWG file, so it’s a lot harder to corrupt your drawing. Instead, the Overrule API will only customize an entity when your application is loaded. (Although, you can save data used by your Overrule as Xdata or in Xrecords).
As a simple example, you can overrule an entity’s worldDraw function and draw your own graphical representation instead. (In the simple sample we demonstrated at Developer Days, we took a Line and turned it into a Thermometer (see image).
Image: Two Lines – Can you tell which one has been Overruled? ;-).
The Overrule API is available in ObjectARX (C++) and .NET. Here’s a simple VB.NET example of how you’d create an overrule…
First, create your custom Overrule class, inheriting from one of the available Overrules, and overriding the functions you want to overrule. In this case, we’re overruling an entity’s WorldDraw function. WorldDraw is part of the DrawableOverrule.
Imports Autodesk.AutoCAD.GraphicsInterface
Public Class MyDrawOverrule
Inherits DrawableOverrule
'This is the function that gets called to add/replace
'an entity's WorldDraw graphics
Public Overrides Function WorldDraw( _
ByVal drawable As Drawable, _
ByVal wd As WorldDraw) As Boolean
'Draw my own graphics here ...
'Call the object's own worldDraw function (if you want to)
Return MyBase.WorldDraw(drawable, wd)
End Function
End Class
Next, instantiate your Overrule, add it to the entity you want to overrule, and turn Overruling on. (You can also specify how the overrule is applied – you can apply it to every object of that type, apply it depending on Xdata or Xrecords, maintain a list of ObjectIds of entities to be overruled, or define your own custom filter).
'mDrawOverrule is a class member variable
'we declared elsewhere
mDrawOverrule = New MyDrawOverrule
'Add the Overrule to the entity class - in this case Line
Overrule.AddOverrule( _
RXObject.GetClass(GetType(Line)), _
mDrawOverrule, False)
'Optional - specify filter
'(In this case we only apply overrule to Lines with entry
' named "RDS_MyData" in Extension Dictionary)
mDrawOverrule.SetExtensionDictionaryEntryFilter("RDS_MyData")
'Turn overruling on
Overrule.Overruling = True
And that’s all there is to it.
You can find a (simple) working Overrule sample with the Developer Days material posted on the ADN website. We’ll be extending that sample soon and using it as the basis of a webcast after AutoCAD 2010 has shipped. And look at the ‘Behavior Overrules’ section of the ObjectARX Developers Guide for information on the ObjectARX implementation of this API, and for details of methods affected by this API.
Freeform Modeling API
3D modeling in AutoCAD tends to be a bit ‘blocky’. It’s hard to create a shape that looks really organic. That’s where Freeform modeling comes in. It’s hard to describe succinctly the power of this feature, so I’d encourage you to review Heidi’s product demonstration . The basic idea is to take a solid or mesh, twist it around a bit by pushing and pulling at its edges, vertices and faces, and then smooth it and crease it. The smoothing is performed usingSubdivision – we use the Catmull-Clark algorithm that is already being used by other Autodesk products.
The API centers on the Sub-division mesh object – AcDbSubDMesh in ObjectARX, DatabaseServices.SubDMesh in .NET, and AcadSubDMesh in ActiveX. The API allows you to do essentially everything a user can through the UI. Here’s a simple VB.NET sample showing how to generate a SubDMesh from a Solid3d and then apply level 1 smoothing to it.
Imports Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.EditorInput
Imports Autodesk.AutoCAD.Geometry
Imports Autodesk.AutoCAD.Runtime
Public Class FreeFormSample
<CommandMethod("CREATEMESH")> _
Public Sub MySub()
'Select a solid.
Dim ed As Editor = _
Application.DocumentManager.MdiActiveDocument.Editor
Dim opts As _
New PromptEntityOptions(vbCrLf + "Select Solid:")
opts.SetRejectMessage(vbCrLf & "That's not a solid!")
opts.AddAllowedClass(GetType(Solid3d), False)
Dim res As PromptEntityResult = ed.GetEntity(opts)
'Exit sub if user cancelled selection.
If res.Status <> PromptStatus.OK Then Exit Sub
'Usual transaction stuff
Dim db As Database = _
Application.DocumentManager.MdiActiveDocument.Database
Using tr As Transaction = _
db.TransactionManager.StartTransaction
Dim mySolid As Solid3d = _
tr.GetObject( _
res.ObjectId, _
OpenMode.ForRead, False)
Dim ext As Extents3d = mySolid.Bounds
Dim vec As Vector3d = (ext.MaxPoint - ext.MinPoint)
'Define params governing mesh generation algorithm
'(See ObjectARX helpfiles for explanation of params –
' you may need to change them depending on the scale
' of the solid)
Dim myFaceterData As _
New MeshFaceterData( _
0.01 * vec.Length, _
40 * Math.PI / 180, _
2, 2, 15, 5, 5, 0)
'Create new mesh from solid (smoothing level 1)
Dim meshData As MeshDataCollection = _
SubDMesh.GetObjectMesh(mySolid, myFaceterData)
Dim myMesh As New SubDMesh
myMesh.SetSubDMesh( _
meshData.VertexArray, meshData.FaceArray, 1)
'Add mesh to database. (Don't remove solid).
myMesh.SetDatabaseDefaults()
Dim btr As BlockTableRecord = _
tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite)
btr.AppendEntity(myMesh)
tr.AddNewlyCreatedDBObject(myMesh, True)
'Our work here is done
tr.Commit()
End Using
End Sub
End Class
In the next post we'll look at the Parametric Drawing API, CUI API Enhancements, RibbonBar Controls, PDF Underlays and the new AutoCAD .NET Developer's Guide.
九、
July 27, 2009
Providing information on AutoCAD objects in a tooltip using .NET
One of the responses to my last post on the “Plugin of the Month” asked about showing information on an AutoCAD drawing object via a tooltip. Other than using the standard rollover tooltip properties mechanism, as shown in this previous post, the best way to achieve this is via a PointMonitor.
In the below C# code we check which object ore objects are being hovered over, get information about those objects and add them to the tooltip.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
public class PointMonitorTooltips
{
public static void StartMonitor()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.PointMonitor +=
new PointMonitorEventHandler(ed_PointMonitor);
}
public static void StopMonitor()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.PointMonitor -=
new PointMonitorEventHandler(ed_PointMonitor);
}
static void ed_PointMonitor(object sender, PointMonitorEventArgs e)
{
Editor ed = (Editor)sender;
Document doc = ed.Document;
if (!e.Context.PointComputed)
return;
try
{
// Determine the size of the selection aperture
short pickbox =
(short)Application.GetSystemVariable("PICKBOX");
Point2d extents =
e.Context.DrawContext.Viewport.GetNumPixelsInUnitSquare(
e.Context.ComputedPoint
);
double boxWidth = pickbox / extents.X;
Vector3d vec =
new Vector3d(boxWidth / 2, boxWidth / 2, 0.0);
// Do a crossing selection using a centred box the
// size of the aperture
PromptSelectionResult pse =
ed.SelectCrossingWindow(
e.Context.ComputedPoint - vec,
e.Context.ComputedPoint + vec
);
if (pse.Status != PromptStatus.OK ||
pse.Value.Count <= 0)
return;
// Go through the results of the selection
// and detect the curves
string curveInfo = "";
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
// Open each object, one by one
ObjectId[] ids = pse.Value.GetObjectIds();
foreach (ObjectId id in ids)
{
DBObject obj =
tr.GetObject(id, OpenMode.ForRead);
if (obj != null)
{
// If it's a curve, get its length
Curve cv = obj as Curve;
if (cv != null)
{
double length =
cv.GetDistanceAtParameter(cv.EndParam);
// Then add a message including the object
// type and its length
curveInfo +=
obj.GetType().Name + "'s length: " +
string.Format("{0:F}", length) + "\n";
}
}
}
// Cheaper than aborting
tr.Commit();
}
// Add the tooltip of the lengths of the curves detected
if (curveInfo != "")
e.AppendToolTipText(curveInfo);
}
catch
{
// Not sure what we might get here, but not real action
// needed (worth adding an Exception parameter and a
// breakpoint, in case things need investigating).
}
}
}During the command we perform a selection, using Editor.SelectCrossingWindow() specifying a window based on the selected point, using a window that’s about the size of the selection aperture (at least that’s what I’m trying to do: let me know if you’ve got a better solution for this). Having some amount of “fuzz” in the selection allows us to not have to hover very precisely over the object, which may or may not be what you’re looking for.
We then iterate through the objects, gathering information about the objects we care about. In this case we’re looking for curves: when we find one, we add its length (along with its object type) to the string we eventually add to the tooltip via AppendToolTipText().
There are two commands: SM adds the monitor, and XM removes it. Bear in mind that the event fires for all sorts of input events - including keystrokes – so you can actually cause problems unless you’re careful. As an example, I wasn’t previously checking the PointComputed status of the PointManager’s context, which is false if we’re looking at keyboard input. The code would go ahead and try to select geometry, which – for some reason unknown to me – cause the character in the keystroke buffer to be lost. Which meant that entering any command would only result in the last command being executed (via the return being processed – that one got through, OK :-). All this to point out that care should be taken when relying on events that are so integrated so deeply into AutoCAD’s user input mechanism.
PointMonitor tooltip over a single line So let’s see what we get after NETLOADing the module, running the SM command and hovering over a curve, in this case a line. We see a tooltip appear containing the line’s length.
And if we do the same with a lot more intersecting objects, we see that we also get the PointMonitor tooltip over lots of different curvesopportunity to provide information on those. It’s ultimately our choice how we manage the selection process and what we do with the results (although please don’t assume you can attempt everything from this kind of very frequently fired event: there are a number of areas where you may find things failing, especially if there’s any interactive component).
Update:
Thanks to Tony Tanzillo for keeping me honest (see his comments, below). The below code addresses a few issues with the version shown above, and should be used instead.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
public class PointMonitorTooltips
{
public static void StartMonitor()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.PointMonitor +=
new PointMonitorEventHandler(ed_PointMonitor);
}
public static void StopMonitor()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.TurnForcedPickOn();
ed.PointMonitor -=
new PointMonitorEventHandler(ed_PointMonitor);
}
static void ed_PointMonitor(object sender, PointMonitorEventArgs e)
{
Editor ed = (Editor)sender;
Document doc = ed.Document;
try
{
FullSubentityPath[] paths =
e.Context.GetPickedEntities();
// Go through the results of the selection
// and detect the curves
string curveInfo = "";
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
// Open each object, one by one
foreach (FullSubentityPath path in paths)
{
ObjectId[] ids = path.GetObjectIds();
if (ids.Length > 0)
{
ObjectId id = ids;
DBObject obj =
tr.GetObject(id, OpenMode.ForRead);
if (obj != null)
{
// If it's a curve, get its length
Curve cv = obj as Curve;
if (cv != null)
{
double length =
cv.GetDistanceAtParameter(cv.EndParam) -
cv.GetDistanceAtParameter(cv.StartParam);
// Then add a message including the object
// type and its length
curveInfo +=
obj.GetType().Name + "'s length: " +
string.Format("{0:F}", length) + "\n";
}
}
}
}
// Cheaper than aborting
tr.Commit();
}
// Add the tooltip of the lengths of the curves detected
if (curveInfo != "")
e.AppendToolTipText(curveInfo);
}
catch
{
// Not sure what we might get here, but not real action
// needed (worth adding an Exception parameter and a
// breakpoint, in case things need investigating).
}
}
}
页:
1
[2]