Kean专题(10)—Notification_Events
本帖最后由 作者 于 2009-6-12 7:33:30 编辑原帖:http://through-the-interface.typepad.com/through_the_interface/notification_events
一、圆链
November 27, 2006
Linking Circles, Part 1: Using .NET events to relate AutoCAD geometry
I received this question some time ago from Paul Richardson from CAD System Engineering:
I have never been sure when to update objects programmatically. An example would be a user edits an entity that I’m tracking and I need to edit another entity in reaction to that change. Is there a standard used to cache the handle, and make the change.
Doesn’t seem editing of entities should be done in the event that is watching for the change. When/how does one do it? Doesn’t seem to be any info on a standard for this.
This is such an excellent question I'm going to spend a number of posts answering it. :-)
Introduction
But first, time for a little nostalgia. One of my first ObjectARX projects was back in 1996 - I'd been using LISP and ADS for several years by that point, but I decided what I really needed was a nice, juicy problem to help me immerse myself in ObjectARX. Along with a colleague at the time, I came up with the idea of using ObjectARX's notification mechanism to link circles together in a chain.
The idea was essentially that you "link" sets of two circles together, and whenever you move one of these circles, the other circle moves in the most direct line to stay attached to it. You would then be able to build up "chains" of linked circles, and the movement of the head of the chain would cause the rest of the chain to follow, with a ripple of notification events modifying one circle after the other.
It was my first major ObjectARX undertaking, so I was fairly heavy-handed with the architecture: each "link" was maintained by two persistent reactors - one attached to each of the linked entities. There were also a number of other reactors and objects involved in the whole system which, in spite of it's weight, worked pretty well. I demoed the sample to developers at a number of different events, to show the power of ObjectARX, and also built it into my first AutoCAD OEM demo application (called SnakeCAD :-).
Anyway - I hadn't thought about this code for several years, but then I received Paul's question and by chance stumbled across the source attached to an old email, so thought I'd spend some time reimplementing the system in .NET. I was able to recode the whole thing in less than a day, partly thanks to the additional experience of being 10 years longer-in-the-tooth, but mainly because of the advantages of using a much more modern development environment.
I'm going to serialize the code over a few posts. The first shows the basic implementation, which should allow you to focus on how the events do their stuff, and I'll later on deal with persistence of our data and some more advanced features (such as automatic linking and support for other circular - even spherical - objects).
The Basic Application
For this application I'm going to try something different, by putting line numbers in the below code (to make the explanation simpler) and providing the main class file as a download.
First, a little on the approach:
The basic application defines one single command - "LINK" (lines 162-194). This command asks the user to select two circles, which it then links together. It does this by using a special "link manager" object (the LinkedObjectManager class is defined from lines 23 to 115), which is used to maintain the references between the various circles.
This LinkedObjectManager stores one-to-many relationships by maintaining a Dictionary, mapping between ObjectIds and ObjectIdCollections. This means that any particular circle can be linked to multiple other circles. The relationships also get added bi-directionally, so the LinkedObjectManager will create a backwards link when it creates the forwards one (lines 37-38).
The linking behaviour is maintained by two main event callbacks: the first is Database.ObjectModified(), which is called whenever an object stored in the active drawing has been changed in some way. This event callback is implemented between lines 196 and 206. All it does is check whether the object that has been modified is one that is being "managed" by our link manager - if so, we add its ID to the list of entities to update later on (the collection that is declared on line 122).
This is really the answer to Paul's question: we store the ObjectId in a list that will get picked up in the Editor.CommandEnded() callback, where we go and update the various objects. My original implementation didn't do that: it opened the objects directly using Open()/Close() (which are marked as obsolete in the .NET API, as we're encouraging the use of Transactions instead), and made the changes right then. Overall the implementation in this version is safer and, I feel, more elegant - CommandEnded() is really the way to go for this kind of operation.
The Editor.CommandEnded() callback is implemented between lines 219 and 227, and calls through to another function (UpdateLinkedEntities()) to do the heavy lifting (lines 230-316). This function checks the geometry of the linked objects - I've tried to keep the code fairly generic to make it easier for us to extend this later to handle non-circles - and moves the second one closer to the first one. This in turn fires the Database.ObjectModified() event again, which adds this entity's ObjectId into the list of entities to update. What's interesting about this implementation is that the foreach loop that is making the calls to UpdateLinkedEntities() for each object in the list (lines 222-225), will also take into account the newly added entities. This allows the change to ripple through the entire chain of circles.
Here's the C# code:
using System;
using System.Collections;
using System.Collections.Generic;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
[assembly:
CommandClass(
typeof(
AsdkLinkingLibrary.LinkingCommands
)
)
]
namespace AsdkLinkingLibrary
{
/// <summary>
/// Utility class to manage links between objects
/// </summary>
public class LinkedObjectManager
{
Dictionary<ObjectId, ObjectIdCollection> m_dict;
// Constructor
public LinkedObjectManager()
{
m_dict =
new Dictionary<ObjectId,ObjectIdCollection>();
}
// Create a bi-directional link between two objects
public void LinkObjects(ObjectId from, ObjectId to)
{
CreateLink(from, to);
CreateLink(to, from);
}
// Helper function to create a one-way
// link between objects
private void CreateLink(ObjectId from, ObjectId to)
{
ObjectIdCollection existingList;
if (m_dict.TryGetValue(from, out existingList))
{
if (!existingList.Contains(to))
{
existingList.Add(to);
m_dict.Remove(from);
m_dict.Add(from, existingList);
}
}
else
{
ObjectIdCollection newList =
new ObjectIdCollection();
newList.Add(to);
m_dict.Add(from, newList);
}
}
// Remove bi-directional links from an object
public void RemoveLinks(ObjectId from)
{
ObjectIdCollection existingList;
if (m_dict.TryGetValue(from, out existingList))
{
m_dict.Remove(from);
foreach (ObjectId id in existingList)
{
RemoveFromList(id, from);
}
}
}
// Helper function to remove an object reference
// from a list (assumes the overall list should
// remain)
private void RemoveFromList(
ObjectId key,
ObjectId toremove
)
{
ObjectIdCollection existingList;
if (m_dict.TryGetValue(key, out existingList))
{
if (existingList.Contains(toremove))
{
existingList.Remove(toremove);
m_dict.Remove(key);
m_dict.Add(key, existingList);
}
}
}
// Return the list of objects linked to
// the one passed in
public ObjectIdCollection GetLinkedObjects(
ObjectId from
)
{
ObjectIdCollection existingList;
m_dict.TryGetValue(from, out existingList);
return existingList;
}
// Check whether the dictionary contains
// a particular key
public bool Contains(ObjectId key)
{
return m_dict.ContainsKey(key);
}
}
/// <summary>
/// This class defines our commands and event callbacks.
/// </summary>
public class LinkingCommands
{
LinkedObjectManager m_linkManager;
ObjectIdCollection m_entitiesToUpdate;
public LinkingCommands()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
db.ObjectModified +=
new ObjectEventHandler(OnObjectModified);
db.ObjectErased +=
new ObjectErasedEventHandler(OnObjectErased);
doc.CommandEnded +=
new CommandEventHandler(OnCommandEnded);
m_linkManager = new LinkedObjectManager();
m_entitiesToUpdate = new ObjectIdCollection();
}
~LinkingCommands()
{
try
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
db.ObjectModified -=
new ObjectEventHandler(OnObjectModified);
db.ObjectErased -=
new ObjectErasedEventHandler(OnObjectErased);
doc.CommandEnded +=
new CommandEventHandler(OnCommandEnded);
}
catch(System.Exception)
{
// The document or database may no longer
// be available on unload
}
}
// Define "LINK" command
public void LinkEntities()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptEntityOptions opts =
new PromptEntityOptions(
"\nSelect first circle to link: "
);
opts.AllowNone = true;
opts.SetRejectMessage(
"\nOnly circles can be selected."
);
opts.AddAllowedClass(typeof(Circle), false);
PromptEntityResult res = ed.GetEntity(opts);
if (res.Status == PromptStatus.OK)
{
ObjectId from = res.ObjectId;
opts.Message =
"\nSelect second circle to link: ";
res = ed.GetEntity(opts);
if (res.Status == PromptStatus.OK)
{
ObjectId to = res.ObjectId;
m_linkManager.LinkObjects(from, to);
m_entitiesToUpdate.Add(from);
}
}
}
// Define callback for Database.ObjectModified event
private void OnObjectModified(
object sender, ObjectEventArgs e)
{
ObjectId id = e.DBObject.ObjectId;
if (m_linkManager.Contains(id) &&
!m_entitiesToUpdate.Contains(id))
{
m_entitiesToUpdate.Add(id);
}
}
// Define callback for Database.ObjectErased event
private void OnObjectErased(
object sender, ObjectErasedEventArgs e)
{
if (e.Erased)
{
m_linkManager.RemoveLinks(e.DBObject.ObjectId);
}
}
// Define callback for Document.CommandEnded event
private void OnCommandEnded(
object sender, CommandEventArgs e)
{
foreach (ObjectId id in m_entitiesToUpdate)
{
UpdateLinkedEntities(id);
}
m_entitiesToUpdate.Clear();
}
// Helper function for OnCommandEnded
private void UpdateLinkedEntities(ObjectId from)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
Database db = doc.Database;
ObjectIdCollection linked =
m_linkManager.GetLinkedObjects(from);
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
try
{
Point3d firstCenter;
Point3d secondCenter;
double firstRadius;
double secondRadius;
Entity ent =
(Entity)tr.GetObject(from, OpenMode.ForRead);
if (GetCenterAndRadius(
ent,
out firstCenter,
out firstRadius
)
)
{
foreach (ObjectId to in linked)
{
Entity ent2 =
(Entity)tr.GetObject(to, OpenMode.ForRead);
if (GetCenterAndRadius(
ent2,
out secondCenter,
out secondRadius
)
)
{
Vector3d vec = firstCenter - secondCenter;
if (!vec.IsZeroLength())
{
// Only move the linked circle if it's not
// already near enough
double apart =
vec.Length - (firstRadius + secondRadius);
if (apart < 0.0)
apart = -apart;
if (apart > 0.00001)
{
ent2.UpgradeOpen();
ent2.TransformBy(
Matrix3d.Displacement(
vec.GetNormal() * apart
)
);
}
}
}
}
}
}
catch (System.Exception ex)
{
Autodesk.AutoCAD.Runtime.Exception ex2 =
ex as Autodesk.AutoCAD.Runtime.Exception;
if (ex2 != null &&
ex2.ErrorStatus != ErrorStatus.WasOpenForUndo)
{
ed.WriteMessage(
"\nAutoCAD exception: {0}", ex2
);
}
else if (ex2 == null)
{
ed.WriteMessage(
"\nSystem exception: {0}", ex
);
}
}
tr.Commit();
}
}
// Helper function to get the center and radius
// for all supported circular objects
private bool GetCenterAndRadius(
Entity ent,
out Point3d center,
out double radius
)
{
// For circles it's easy...
Circle circle = ent as Circle;
if (circle != null)
{
center = circle.Center;
radius = circle.Radius;
return true;
}
else
{
// Throw in some empty values...
// Returning false indicates the object
// passed in was not useable
center = Point3d.Origin;
radius = 0.0;
return false;
}
}
}
}Here's what happens when you execute the LINK command on some circles you've drawn...
Some circles:
After the LINK command has been used to link them together, two-by-two:
Now grip-move the head of the chain:
And here's the result - the chain moves to remain attached to the head:
<p>November 29, 2006<br/>Linking Circles, Part 2: Getting persistent<br/>In the previous post we looked at some code that created chains of circles, linking them together using .NET events (a technique that can be used to maintain other types of geometric relationship, of course). In this post, we're going to extend our sample to support persistence of the link data in the AutoCAD database.</p><p>Firstly, here's the updated source file. Below I've posted the code, once again with line numbers - but this time the lines that have changed or been added since the previous post are marked in red. This should highlight the modified sections of the code.</p><p>Looking at the specific changes, the major updates are to our LinkedObjectManager class: between lines 124 and 454 there's some additional protocol to support persistence. Primarily the obviously named SaveToDatabase() and LoadFromDatabase(), but also some support functions: AddValidatedLinks(), which we use on loading data from the drawing to make sure only valid links get resurrected, and GetLinkDictionaryId(), which we use to identify (and create, if needed) the dictionary we're using to store the link data.</p><p>Some information on how the data is being stored: I decided to go ahead and use Xrecords to store the data. Xrecords are flexible, non-graphical data containers that can be stored in dictionaries (of type DBDictionary) in the DWG file. They are also supported natively by AutoCAD, so there's no need for a DBX module to help you access the data. DBDictionaries are basically persistent maps between keys and values. A simple "LINKXREC" gets suffixed by a counter ("0", "1", "2", etc.) to store our Xrecords - this way we know exactly where to look for them.</p><p>It's worth taking the trouble of creating nested dictionaries - an outer one for the "company", and an inner one for the "application". The outer one must, of course, be prefixed with your Registered Developer Symbol (RDS) to prevent conflicts with other applications. Having an inner dictionary just gives us greater flexibility if we later choose to extend the amount of custom data we store in the drawing file.</p><p>The rest of the changes are to add some simple commands - LOADLINKS and SAVELINKS - to call through to our new persistence protocol. There's also an event handler for BeginSave(), which will automatically put our data into the drawing file when it's about to be saved. This type of automatic persistence is clearly very convenient: an exercise I've left for the reader is to automatically load in the data when it exists. The idea would be to respond to a drawing load event (for instance), check whether our data is there (for which we have a very helpful function, GetLinkDictionaryId()) and then prompt the user whether our data should be loaded (or simply go and do it, depending on the extent to which you want to insulate your users from this kind of decision). The implementation is there, it should be fairly trivial to hook the pieces together.</p><p>Another note about the persistence of our data: it should be obvious by now, but we're only using the DBDictionary of Xrecords to store our data - at runtime we use an in-memory dictionary mapping ObjectIds to collections of ObjectIds. This means the data - as you create links between circles - could get out-of-sync with what is stored in the drawing, especially if we were just relying on a command being invoked to save the data.</p><p>If you're interested in checking out how the data is stored, you should look at the ArxDbg sample on the ObjectARX SDK (under samples/database/ARXDBG). This invaluable sample takes the lid off the structure of the drawing database, allowing you to see what is stored and where. The sample also contains some very useful code, showing how to use even some of the more obscure parts of ObjectARX.</p><p>Here's what we see when we use the SNOOPDB command from the ArxDbg sample to take a look at the contents of our custom dictionary:</p><p></p> December 01, 2006
Linking Circles, Part 3: Automatic linking on circle creation
In the previous posts we looked at some code to link AutoCAD entities via .NET events, and how to persist the link data in the drawing file.
This post extends the previous code to automatically link circles into the head of the chain, as circles are drawn by the user. The changes to the project are relatively modest compared to last time. Once again, the source is both available for download and listed below with changed line-numbers in red.
Some notes on the changes:
First we declare some new variables in our command class: a boolean (m_autolink - line 463) which tells us whether automatic linking is "on" or "off", and an ObjectId (m_lastEntity - line 464), which we will use to find the most recently created, linked object. We could have made this a setting in our LinkedObjectManager class, should we have wanted to make this a persistent setting, for instance, but for simplicity's sake we'll leave it in the command class for now.
We have to register (lines 475-476) and unregister (lines 497-498) a handler for another event - OnObjectAppended(). It's via this callback that we're informed that a new object has been added to the drawing.
Next we define our AUTOLINK command (lines 547-563). This simply toggles the m_autolink setting between true and false (or on and off). We might have chosen to display the current setting and ask whether the user wanted to change it to on or off, but frankly that seemed like overkill. If you don't like what you've set it to, you can just call the command again. :-)
A minor change was needed to OnObjectErased(), to set the value of m_lastEntity to Null, should the entity be erased. This also gets caught by a change in the code right at the end, but it's cleaner coding to make the behaviour right here, also.
Next we have the guts of our implementation (such that it is), which is the OnObjectAppended() callback definition (lines 612-636). Here we check whether the object added is a circle, and if so, we either link it to the last one added (stored in m_lastEntity), or - if the value of m_lastEntity is Null, for whatever reason - then we simply make it the next object to be linked in, and leave it at that.
And finally there's a minor change I added to more elegantly support UNDO (which also applies to the code in the first two posts, although I won't go and update them now). Because we're not persisting the state of our links synchronously in the drawing database, they don't automatically participate in the undo mechanism (e.g. if the user uses the UNDO command, we would have to do a little extra work to recreate the correct "last object to link to" settings). Rather than implement the equivalent of our own undo mechanism, I decided not to bother, and simply made sure that when a link is to an erased object, we simply give up (without any error message). This shouldn't happen very often - as we have our OnObjectErased() callback, but you never know. It does mean that the "bad" links might continue to exist in our LinkedObjectManager, they just won't work. The next time the data is saved and reloaded, though, these links effectively get purged. To really make this a production-ready app, I feel a little more attention is needed in this area... that said, the foundation is certainly there for you to work from (just please test thoroughly for your specific situation, of course).
Now for the C# code:
using System;
using System.Collections;
using System.Collections.Generic;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
[assembly:
CommandClass(
typeof(
AsdkLinkingLibrary.LinkingCommands
)
)
]
namespace AsdkLinkingLibrary
{
/// <summary>
/// Utility class to manage and save links
/// between objects
/// </summary>
public class LinkedObjectManager
{
const string kCompanyDict =
"AsdkLinks";
const string kApplicationDict =
"AsdkLinkedObjects";
const string kXrecPrefix =
"LINKXREC";
Dictionary<ObjectId, ObjectIdCollection> m_dict;
// Constructor
public LinkedObjectManager()
{
m_dict =
new Dictionary<ObjectId,ObjectIdCollection>();
}
// Create a bi-directional link between two objects
public void LinkObjects(ObjectId from, ObjectId to)
{
CreateLink(from, to);
CreateLink(to, from);
}
// Helper function to create a one-way
// link between objects
private void CreateLink(ObjectId from, ObjectId to)
{
ObjectIdCollection existingList;
if (m_dict.TryGetValue(from, out existingList))
{
if (!existingList.Contains(to))
{
existingList.Add(to);
m_dict.Remove(from);
m_dict.Add(from, existingList);
}
}
else
{
ObjectIdCollection newList =
new ObjectIdCollection();
newList.Add(to);
m_dict.Add(from, newList);
}
}
// Remove bi-directional links from an object
public void RemoveLinks(ObjectId from)
{
ObjectIdCollection existingList;
if (m_dict.TryGetValue(from, out existingList))
{
m_dict.Remove(from);
foreach (ObjectId id in existingList)
{
RemoveFromList(id, from);
}
}
}
// Helper function to remove an object reference
// from a list (assumes the overall list should
// remain)
private void RemoveFromList(
ObjectId key,
ObjectId toremove
)
{
ObjectIdCollection existingList;
if (m_dict.TryGetValue(key, out existingList))
{
if (existingList.Contains(toremove))
{
existingList.Remove(toremove);
m_dict.Remove(key);
m_dict.Add(key, existingList);
}
}
}
// Return the list of objects linked to
// the one passed in
public ObjectIdCollection GetLinkedObjects(
ObjectId from
)
{
ObjectIdCollection existingList;
m_dict.TryGetValue(from, out existingList);
return existingList;
}
// Check whether the dictionary contains
// a particular key
public bool Contains(ObjectId key)
{
return m_dict.ContainsKey(key);
}
// Save the link information to a special
// dictionary in the database
public void SaveToDatabase(Database db)
{
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
ObjectId dictId =
GetLinkDictionaryId(db, true);
DBDictionary dict =
(DBDictionary)tr.GetObject(
dictId,
OpenMode.ForWrite
);
int xrecCount = 0;
foreach (
KeyValuePair<ObjectId, ObjectIdCollection> kv
in m_dict
)
{
// Prepare the result buffer with our data
ResultBuffer rb =
new ResultBuffer(
new TypedValue(
(int)DxfCode.SoftPointerId,
kv.Key
)
);
int i = 1;
foreach (ObjectId id in kv.Value)
{
rb.Add(
new TypedValue(
(int)DxfCode.SoftPointerId + i,
id
)
);
i++;
}
// Update or create an xrecord to store the data
Xrecord xrec;
bool newXrec = false;
if (dict.Contains(
kXrecPrefix + xrecCount.ToString()
)
)
{
// Open the existing object
DBObject obj =
tr.GetObject(
dict.GetAt(
kXrecPrefix + xrecCount.ToString()
),
OpenMode.ForWrite
);
// Check whether it's an xrecord
xrec = obj as Xrecord;
if (xrec == null)
{
// Should never happen
// We only store xrecords in this dict
obj.Erase();
xrec = new Xrecord();
newXrec = true;
}
}
// No object existed - create a new one
else
{
xrec = new Xrecord();
newXrec = true;
}
xrec.XlateReferences = true;
xrec.Data = (ResultBuffer)rb;
if (newXrec)
{
dict.SetAt(
kXrecPrefix + xrecCount.ToString(),
xrec
);
tr.AddNewlyCreatedDBObject(xrec, true);
}
xrecCount++;
}
// Now erase the left-over xrecords
bool finished = false;
do
{
if (dict.Contains(
kXrecPrefix + xrecCount.ToString()
)
)
{
DBObject obj =
tr.GetObject(
dict.GetAt(
kXrecPrefix + xrecCount.ToString()
),
OpenMode.ForWrite
);
obj.Erase();
}
else
{
finished = true;
}
xrecCount++;
} while (!finished);
tr.Commit();
}
}
// Load the link information from a special
// dictionary in the database
public void LoadFromDatabase(Database db)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
// Try to find the link dictionary, but
// do not create it if one isn't there
ObjectId dictId =
GetLinkDictionaryId(db, false);
if (dictId.IsNull)
{
ed.WriteMessage(
"\nCould not find link dictionary."
);
return;
}
// By this stage we can assume the dictionary exists
DBDictionary dict =
(DBDictionary)tr.GetObject(
dictId, OpenMode.ForRead
);
int xrecCount = 0;
bool done = false;
// Loop, reading the xrecords one-by-one
while (!done)
{
if (dict.Contains(
kXrecPrefix + xrecCount.ToString()
)
)
{
ObjectId recId =
dict.GetAt(
kXrecPrefix + xrecCount.ToString()
);
DBObject obj =
tr.GetObject(recId, OpenMode.ForRead);
Xrecord xrec = obj as Xrecord;
if (xrec == null)
{
ed.WriteMessage(
"\nDictionary contains non-xrecords."
);
return;
}
int i = 0;
ObjectId from = new ObjectId();
ObjectIdCollection to =
new ObjectIdCollection();
foreach (TypedValue val in xrec.Data)
{
if (i == 0)
from = (ObjectId)val.Value;
else
{
to.Add((ObjectId)val.Value);
}
i++;
}
// Validate the link info and add it to our
// internal data structure
AddValidatedLinks(db, from, to);
xrecCount++;
}
else
{
done = true;
}
}
tr.Commit();
}
}
// Helper function to validate links before adding
// them to the internal data structure
private void AddValidatedLinks(
Database db,
ObjectId from,
ObjectIdCollection to
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
try
{
ObjectIdCollection newList =
new ObjectIdCollection();
// Open the "from" object
DBObject obj =
tr.GetObject(from, OpenMode.ForRead, false);
if (obj != null)
{
// Open each of the "to" objects
foreach (ObjectId id in to)
{
DBObject obj2;
try
{
obj2 =
tr.GetObject(id, OpenMode.ForRead, false);
// Filter out the erased "to" objects
if (obj2 != null)
{
newList.Add(id);
}
}
catch (System.Exception)
{
ed.WriteMessage(
"\nFiltered out link to an erased object."
);
}
}
// Only if the "from" object and at least
// one "to" object exist (and are unerased)
// do we add an entry for them
if (newList.Count > 0)
{
m_dict.Add(from, newList);
}
}
}
catch (System.Exception)
{
ed.WriteMessage(
"\nFiltered out link from an erased object."
);
}
tr.Commit();
}
}
// Helper function to get (optionally create)
// the nested dictionary for our xrecord objects
private ObjectId GetLinkDictionaryId(
Database db,
bool createIfNotExisting
)
{
ObjectId appDictId = ObjectId.Null;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
DBDictionary nod =
(DBDictionary)tr.GetObject(
db.NamedObjectsDictionaryId,
OpenMode.ForRead
);
// Our outer level ("company") dictionary
// does not exist
if (!nod.Contains(kCompanyDict))
{
if (!createIfNotExisting)
return ObjectId.Null;
// Create both the "company" dictionary...
DBDictionary compDict = new DBDictionary();
nod.UpgradeOpen();
nod.SetAt(kCompanyDict, compDict);
tr.AddNewlyCreatedDBObject(compDict, true);
// ... and the inner "application" dictionary.
DBDictionary appDict = new DBDictionary();
appDictId =
compDict.SetAt(kApplicationDict, appDict);
tr.AddNewlyCreatedDBObject(appDict, true);
}
else
{
// Our "company" dictionary exists...
DBDictionary compDict =
(DBDictionary)tr.GetObject(
nod.GetAt(kCompanyDict),
OpenMode.ForRead
);
/// So check for our "application" dictionary
if (!compDict.Contains(kApplicationDict))
{
if (!createIfNotExisting)
return ObjectId.Null;
// Create the "application" dictionary
DBDictionary appDict = new DBDictionary();
compDict.UpgradeOpen();
appDictId =
compDict.SetAt(kApplicationDict, appDict);
tr.AddNewlyCreatedDBObject(appDict, true);
}
else
{
// Both dictionaries already exist...
appDictId = compDict.GetAt(kApplicationDict);
}
}
tr.Commit();
}
return appDictId;
}
}
/// <summary>
/// This class defines our commands and event callbacks.
/// </summary>
public class LinkingCommands
{
LinkedObjectManager m_linkManager;
ObjectIdCollection m_entitiesToUpdate;
bool m_autoLink = false;
ObjectId m_lastEntity = ObjectId.Null;
public LinkingCommands()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
db.ObjectModified +=
new ObjectEventHandler(OnObjectModified);
db.ObjectErased +=
new ObjectErasedEventHandler(OnObjectErased);
db.ObjectAppended +=
new ObjectEventHandler(OnObjectAppended);
db.BeginSave +=
new DatabaseIOEventHandler(OnBeginSave);
doc.CommandEnded +=
new CommandEventHandler(OnCommandEnded);
m_linkManager = new LinkedObjectManager();
m_entitiesToUpdate = new ObjectIdCollection();
}
~LinkingCommands()
{
try
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
db.ObjectModified -=
new ObjectEventHandler(OnObjectModified);
db.ObjectErased -=
new ObjectErasedEventHandler(OnObjectErased);
db.ObjectAppended -=
new ObjectEventHandler(OnObjectAppended);
db.BeginSave -=
new DatabaseIOEventHandler(OnBeginSave);
doc.CommandEnded +=
new CommandEventHandler(OnCommandEnded);
}
catch(System.Exception)
{
// The document or database may no longer
// be available on unload
}
}
// Define "LINK" command
public void LinkEntities()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptEntityOptions opts =
new PromptEntityOptions(
"\nSelect first circle to link: "
);
opts.AllowNone = true;
opts.SetRejectMessage(
"\nOnly circles can be selected."
);
opts.AddAllowedClass(typeof(Circle), false);
PromptEntityResult res = ed.GetEntity(opts);
if (res.Status == PromptStatus.OK)
{
ObjectId from = res.ObjectId;
opts.Message =
"\nSelect second circle to link: ";
res = ed.GetEntity(opts);
if (res.Status == PromptStatus.OK)
{
ObjectId to = res.ObjectId;
m_linkManager.LinkObjects(from, to);
m_lastEntity = to;
m_entitiesToUpdate.Add(from);
}
}
}
// Define "AUTOLINK" command
public void ToggleAutoLink()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
m_autoLink = !m_autoLink;
if (m_autoLink)
{
ed.WriteMessage("\nAutomatic linking turned on.");
}
else
{
ed.WriteMessage("\nAutomatic linking turned off.");
}
}
// Define "LOADLINKS" command
public void LoadLinkSettings()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
m_linkManager.LoadFromDatabase(db);
}
// Define "SAVELINKS" command
public void SaveLinkSettings()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
m_linkManager.SaveToDatabase(db);
}
// Define callback for Database.ObjectModified event
private void OnObjectModified(
object sender, ObjectEventArgs e)
{
ObjectId id = e.DBObject.ObjectId;
if (m_linkManager.Contains(id) &&
!m_entitiesToUpdate.Contains(id))
{
m_entitiesToUpdate.Add(id);
}
}
// Define callback for Database.ObjectErased event
private void OnObjectErased(
object sender, ObjectErasedEventArgs e)
{
if (e.Erased)
{
ObjectId id = e.DBObject.ObjectId;
m_linkManager.RemoveLinks(id);
if (m_lastEntity == id)
{
m_lastEntity = ObjectId.Null;
}
}
}
// Define callback for Database.ObjectAppended event
void OnObjectAppended(object sender, ObjectEventArgs e)
{
Database db = sender as Database;
if (db != null)
{
if (m_autoLink)
{
if (e.DBObject.GetType() == typeof(Circle))
{
ObjectId from = e.DBObject.ObjectId;
if (m_lastEntity == ObjectId.Null)
{
m_lastEntity = from;
}
else
{
m_linkManager.LinkObjects(from, m_lastEntity);
m_lastEntity = from;
m_entitiesToUpdate.Add(from);
}
}
}
}
}
// Define callback for Database.BeginSave event
void OnBeginSave(object sender, DatabaseIOEventArgs e)
{
Database db = sender as Database;
if (db != null)
{
m_linkManager.SaveToDatabase(db);
}
}
// Define callback for Document.CommandEnded event
private void OnCommandEnded(
object sender, CommandEventArgs e)
{
foreach (ObjectId id in m_entitiesToUpdate)
{
UpdateLinkedEntities(id);
}
m_entitiesToUpdate.Clear();
}
// Helper function for OnCommandEnded
private void UpdateLinkedEntities(ObjectId from)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
Database db = doc.Database;
ObjectIdCollection linked =
m_linkManager.GetLinkedObjects(from);
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
try
{
Point3d firstCenter;
Point3d secondCenter;
double firstRadius;
double secondRadius;
Entity ent =
(Entity)tr.GetObject(from, OpenMode.ForRead);
if (GetCenterAndRadius(
ent,
out firstCenter,
out firstRadius
)
)
{
foreach (ObjectId to in linked)
{
Entity ent2 =
(Entity)tr.GetObject(to, OpenMode.ForRead);
if (GetCenterAndRadius(
ent2,
out secondCenter,
out secondRadius
)
)
{
Vector3d vec = firstCenter - secondCenter;
if (!vec.IsZeroLength())
{
// Only move the linked circle if it's not
// already near enough
double apart =
vec.Length - (firstRadius + secondRadius);
if (apart < 0.0)
apart = -apart;
if (apart > 0.00001)
{
ent2.UpgradeOpen();
ent2.TransformBy(
Matrix3d.Displacement(
vec.GetNormal() * apart
)
);
}
}
}
}
}
}
catch (System.Exception ex)
{
Autodesk.AutoCAD.Runtime.Exception ex2 =
ex as Autodesk.AutoCAD.Runtime.Exception;
if (ex2 != null &&
ex2.ErrorStatus !=
ErrorStatus.WasOpenForUndo &&
ex2.ErrorStatus !=
ErrorStatus.WasErased
)
{
ed.WriteMessage(
"\nAutoCAD exception: {0}", ex2
);
}
else if (ex2 == null)
{
ed.WriteMessage(
"\nSystem exception: {0}", ex
);
}
}
tr.Commit();
}
}
// Helper function to get the center and radius
// for all supported circular objects
private bool GetCenterAndRadius(
Entity ent,
out Point3d center,
out double radius
)
{
// For circles it's easy...
Circle circle = ent as Circle;
if (circle != null)
{
center = circle.Center;
radius = circle.Radius;
return true;
}
else
{
// Throw in some empty values...
// Returning false indicates the object
// passed in was not useable
center = Point3d.Origin;
radius = 0.0;
return false;
}
}
}
}Let's take a quick look at this code running. Here's an existing chain that we've created using LINK. We then use AUTOLINK to toggle the automatic linking to on, and start creating circles:
http://through-the-interface.typepad.com/through_the_interface/images/linkedcircles_3_1.png
http://through-the-interface.typepad.com/through_the_interface/images/linkedcircles_3_2.png
http://through-the-interface.typepad.com/through_the_interface/images/linkedcircles_3_3.png
http://through-the-interface.typepad.com/through_the_interface/images/linkedcircles_3_4.png
And that's it for this post. Next time we'll look at adding support for other object types, including 3D (woohoo!).
二、过滤Windows消息
May 30, 2008
Filtering Windows messages inside AutoCAD using .NET
Back when I joined Autodesk in 1995, I worked in European Developer Support with one of the most talented programmers I've met, Markus Kraus. One of Markus' contributions to the R13 ARX SDK (or maybe it was R14?) was a sample called pretranslate, which remained on the SDK up until ObjectARX 2008, under samples/editor/mfcsamps/pretranslate (it was removed from the 2009 SDK when we archived a number of aging samples).
Anyway, with AutoCAD 2009 the API that makes this sample possible has been added to the .NET API, so in homage to Markus' original sample (which I have fond memories of demoing during a number of events around Europe), I decided to translate the original C++ sample to C#.
Here's the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using System.Windows.Interop;
using System;
namespace PreTranslate
{
public class Commands
{
// Keys
const int MK_SHIFT = 4;
const int MK_CONTROL = 8;
// Keyboard messages
const int WM_KEYDOWN = 256;
const int WM_KEYUP = 257;
const int WM_CHAR = 258;
const int WM_SYSKEYDOWN = 260;
const int WM_SYSKEYUP = 261;
// Mouse messages
const int WM_MOUSEMOVE = 512;
const int WM_LBUTTONDOWN = 513;
const int WM_LBUTTONUP = 514;
static long MakeLong(int LoWord, int HiWord)
{
return (HiWord << 16) | (LoWord & 0xffff);
}
static IntPtr MakeLParam(int LoWord, int HiWord)
{
return (IntPtr)MakeLong(LoWord,HiWord);
}
static int HiWord(int Number)
{
return (Number >> 16) & 0xffff;
}
static int LoWord(int Number)
{
return Number & 0xffff;
}
// State used by the VhmouseHandler to filter on
// the vertical or the horizontal
bool vMode;
bool hMode;
int ptx;
int pty;
// Commands to add/remove message handlers
public void Caps()
{
Application.PreTranslateMessage +=
new PreTranslateMessageEventHandler(CapsHandler);
}
public void UnCaps()
{
Application.PreTranslateMessage -=
new PreTranslateMessageEventHandler(CapsHandler);
}
public void Vhmouse()
{
Application.PreTranslateMessage +=
new PreTranslateMessageEventHandler(VhmouseHandler);
}
public void UnVhmouse()
{
Application.PreTranslateMessage -=
new PreTranslateMessageEventHandler(VhmouseHandler);
}
public void WatchCC()
{
Application.PreTranslateMessage +=
new PreTranslateMessageEventHandler(WatchCCHandler);
}
public void UnWatchCC()
{
Application.PreTranslateMessage -=
new PreTranslateMessageEventHandler(WatchCCHandler);
}
public void NoX()
{
Application.PreTranslateMessage +=
new PreTranslateMessageEventHandler(NoXHandler);
}
public void YesX()
{
Application.PreTranslateMessage -=
new PreTranslateMessageEventHandler(NoXHandler);
}
// The event handlers themselves...
// Force alphabetic character entry to uppercase
void CapsHandler(
object sender,
PreTranslateMessageEventArgs e
)
{
// For every lowercase character message,
// reduce it my 32 (which forces it to
// uppercase in ASCII)
if (e.Message.message == WM_CHAR &&
(e.Message.wParam.ToInt32() >= 97 &&
e.Message.wParam.ToInt32() <= 122))
{
MSG msg = e.Message;
msg.wParam =
(IntPtr)(e.Message.wParam.ToInt32() - 32);
e.Message = msg;
}
}
// Force mouse movement to either horizontal or
// vertical
void VhmouseHandler(
object sender,
PreTranslateMessageEventArgs e
)
{
// Only look at mouse messages
if (e.Message.message == WM_MOUSEMOVE ||
e.Message.message == WM_LBUTTONDOWN ||
e.Message.message == WM_LBUTTONUP)
{
// If the left mousebutton is pressed and we are
// filtering horizontal or vertical movement,
// make the position the one we're storing
if ((e.Message.message == WM_LBUTTONDOWN ||
e.Message.message == WM_LBUTTONUP)
&& (vMode ||hMode))
{
MSG msg = e.Message;
msg.lParam = MakeLParam(ptx, pty);
e.Message = msg;
return;
}
// If the Control key is pressed
if (e.Message.wParam.ToInt32() == MK_CONTROL)
{
// If we're already in "vertical" mode,
// set the horizontal component of our location
// to the one we've stored
// Otherwise we set the internal "x" value
// (as this is the first time through)
if (vMode)
{
MSG msg = e.Message;
msg.lParam =
MakeLParam(
ptx,
HiWord(e.Message.lParam.ToInt32())
);
e.Message = msg;
pty = HiWord(e.Message.lParam.ToInt32());
}
else
ptx = LoWord(e.Message.lParam.ToInt32());
vMode = true;
hMode = false;
}
// If the Shift key is pressed
else if (e.Message.wParam.ToInt32() == MK_SHIFT)
{
// If we're already in "horizontal" mode,
// set the vertical component of our location
// to the one we've stored
// Otherwise we set the internal "y" value
// (as this is the first time through)
if (hMode)
{
MSG msg = e.Message;
msg.lParam =
MakeLParam(
LoWord(e.Message.lParam.ToInt32()),
pty
);
e.Message = msg;
ptx = LoWord(e.Message.lParam.ToInt32());
}
else
pty = HiWord(e.Message.lParam.ToInt32());
hMode = true;
vMode = false;
}
else
// Something else was pressed,
// so cancel our filtering
vMode = hMode = false;
}
}
// Watch for Ctrl-C, and display a message
void WatchCCHandler(
object sender,
PreTranslateMessageEventArgs e
)
{
// Check for the Ctrl-C Windows message
if (e.Message.message == WM_CHAR &&
e.Message.wParam.ToInt32() == 3)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
doc.Editor.WriteMessage(
"\nCtrl-C is pressed"
);
}
}
// Filter out use of the letter x/X
void NoXHandler(
object sender,
PreTranslateMessageEventArgs e
)
{
// If lowercase or uppercase x is pressed,
// filter the message by setting the
// Handled property to true
if (e.Message.message == WM_CHAR &&
(e.Message.wParam.ToInt32() == 120 ||
e.Message.wParam.ToInt32() == 88))
e.Handled = true;
}
}
}
To be able to use the System.Windows.Interop namespace, you'll need to add a project reference to WindowsBase.dll. Strangely this can take some finding - at least on my system it wasn't included in the base assembly list. To find it, I browsed to:
C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll
The application defines a number of commands, which are described in this text from the original sample's ReadMe:
This sample shows how to pretranslate AutoCAD messages
before they're processed by AutoCAD.
In order to pre-processe AutoCAD messages, a hook function
needs to be installed. The following commands install
different hook functions.
- vhmouse/unvhmouse
Installs/uninstalls a hook that makes the mouse move only in a
vertical direction if <CTRL> key is pressed, and only in a
horizontal direction if the <SHIFT> key is pressed.
- caps/uncaps
Installs/uninstalls a hook that capitalizes all
letters typed in the command window.
- noX/yes
Installs/uninstalls a hook that filters out the
letters 'x' or 'X'.
- watchCC/unwatchCC
Installs/uninstalls a hook that watchs
for <CTRL>+C key combination to be pressed.
Here's what happens when we run the various commands:
Command: caps
Command: UNCAPS
Command: vhmouse
Command: unvhmouse
Command: watchcc
Command:
Ctrl-C is pressed
Command:
Command: _copyclip
Select objects: *Cancel*
Command: nox
Command: yes
While you can't see all the effects of the various commands from this dump of the command-line, here are some comments and pointers, should you try this sample:
The Shift or Caps Lock keys was not used at all during entry of the command-names
During the vhmouse command, move the mouse around and use the Shift and Ctrl keys to force the movement to horizontal or vertical
The Ctrl key now shows an entity selection cursor in AutoCAD, so I should probably have changed to another key for this, but anyway
Ctrl-C now launches COPYCLIP, but we see the message first
The yes command should obviously be called yesx, but then we can't enter the "x" character after running the nox command. :-)
As a final note: I don't recommend filtering commonly-used keystrokes in your application - your users really won't thank you - but this fun little sample at least shows you the capabilities of the PreTranslate mechanism.
三、多实体相互锚定
August 11, 2008
Anchoring AutoCAD entities to each other using .NET
The code in the following two posts was provided by Jeremy Tammik, from our DevTech team in Europe, who presented it at an advanced custom entity workshop he delivered recently in Prague to rave reviews. I've formatted the code to fit the blog and added some commentary plus steps to see it working. Thank you, Jeremy!
Those of you who are familiar with the workings of AutoCAD Architecture - and especially the Object Modeling Framework - will know of the very cool ability for entities to be anchored to one another. This works because graphical ACA classes derive from a "geo" class, which exposes basic location information in a generic way. While AutoCAD entities don't provide this generic location information, it is, however, possible to implement your own anchoring by depending on the location information exposed by specific classes.
The below example does just this: it takes the simple example of anchoring a circle to a line. This technique is especially useful in .NET as it allows us to build in intelligence for standard AutoCAD entities without the need to implement (much more complex) custom entities. Which is why Jeremy was showing it at a custom entity workshop. :-)
Here's the C# code:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
namespace Anchor
{
/// <summary>
/// Anchor command.
///
/// Demonstrate a simple anchoring system.
///
/// In OMF, an anchor is implemented as a custom object
/// which keeps track of the object ids of the host object
/// and the object anchored to the host.
///
/// Here, we implement a simpler mechanism, which maintains
/// lists of host objects and anchored objects with mappings
/// to each other.
///
/// The host objects are lines, and the anchored objects are
/// circles. Any number of circles can be anchored to a line,
/// but a circle can only be anchored to one line at a time.
///
/// The main command prompts the user to select a circle to
/// anchor and a line to host it. From then on, the circle
/// will remain anchored on that line, regardsless how the
/// user tries to move either the line or the circle.
/// Currently, supported manipulations are the MOVE and
/// GRIP_STRETCH commands.
///
/// The implementation is similar to the simpler Reactor
/// sample, and the same principles about cascaded reactors
/// apply.
///
/// We make the command a non-static method, so that each
/// document has its own instance of the command class.
/// </summary>
public class CmdAnchor
{
static List<string> _commandNames =
new List<string>(
new string[] { "MOVE", "GRIP_STRETCH" }
);
private Document _doc;
private Dictionary<ObjectId, List<ObjectId>>
_mapHostToAnchored;
private Dictionary<ObjectId, ObjectId> _mapAnchoredToHost;
private static ObjectIdCollection _ids;
private static List<double> _pos;
Editor Ed
{
get
{
return _doc.Editor;
}
}
public CmdAnchor()
{
_doc =
Application.DocumentManager.MdiActiveDocument;
_doc.CommandWillStart +=
new CommandEventHandler(doc_CommandWillStart);
_mapHostToAnchored =
new Dictionary<ObjectId, List<ObjectId>>();
_mapAnchoredToHost =
new Dictionary<ObjectId, ObjectId>();
_ids = new ObjectIdCollection();
_pos = new List<double>();
Ed.WriteMessage(
"Anchors initialised for '{0}'. ",
_doc.Name
);
}
bool selectEntity(Type t, out ObjectId id)
{
id = ObjectId.Null;
string name = t.Name.ToLower();
string prompt =
string.Format("Please select a {0}: ", name);
string msg =
string.Format(
"Selected entity is not a {0}, please try again...",
name
);
PromptEntityOptions optEnt =
new PromptEntityOptions(prompt);
optEnt.SetRejectMessage(msg);
optEnt.AddAllowedClass(t, true);
PromptEntityResult resEnt =
Ed.GetEntity(optEnt);
if (PromptStatus.OK == resEnt.Status)
{
id = resEnt.ObjectId;
}
return !id.IsNull;
}
/// <summary>
/// Command to define an anchor between a selected host
/// line and an anchored circle.
/// </summary>
public void Anchor()
{
ObjectId hostId, anchoredId;
if (selectEntity(typeof(Line), out hostId)
&& selectEntity(typeof(Circle), out anchoredId))
{
// Check for previously stored anchors:
if (_mapAnchoredToHost.ContainsKey(anchoredId))
{
Ed.WriteMessage("Previous anchor removed.");
ObjectId oldHostId =
_mapAnchoredToHost;
_mapAnchoredToHost.Remove(anchoredId);
_mapHostToAnchored.Remove(anchoredId);
}
// Add new anchor data:
if (!_mapHostToAnchored.ContainsKey(hostId))
{
_mapHostToAnchored =
new List<ObjectId>();
}
_mapHostToAnchored.Add(anchoredId);
_mapAnchoredToHost.Add(anchoredId, hostId);
// Ensure that anchored object is located on host:
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
Line line =
t.GetObject(hostId, OpenMode.ForRead)
as Line;
Circle circle =
t.GetObject(anchoredId, OpenMode.ForWrite)
as Circle;
Point3d ps = line.StartPoint;
Point3d pe = line.EndPoint;
LineSegment3d segment =
new LineSegment3d(ps, pe);
Point3d p = circle.Center;
circle.Center =
segment.GetClosestPointTo(p).Point;
t.Commit();
}
}
}
void doc_CommandWillStart(
object sender,
CommandEventArgs e
)
{
if (_commandNames.Contains(e.GlobalCommandName))
{
_ids.Clear();
_pos.Clear();
_doc.Database.ObjectOpenedForModify +=
new ObjectEventHandler(_db_ObjectOpenedForModify);
_doc.CommandCancelled +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed +=
new CommandEventHandler(_doc_CommandEnded);
}
}
void removeEventHandlers()
{
_doc.CommandCancelled -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed -=
new CommandEventHandler(_doc_CommandEnded);
_doc.Database.ObjectOpenedForModify -=
new ObjectEventHandler(_db_ObjectOpenedForModify);
}
void _doc_CommandEnded(
object sender,
CommandEventArgs e
)
{
// Remove database reactor before restoring positions
removeEventHandlers();
rollbackLocations();
}
void saveLocation(
ObjectId hostId,
ObjectId anchoredId,
bool hostModified
)
{
if (!_ids.Contains(anchoredId))
{
// If the host was moved, remember the location of
// the anchored object on the host so we can restore
// it.
// If the anchored object was moved, we do not need
// to remember its location of the anchored object,
// because we will simply snap back the the host
// afterwards.
double a = double.NaN;
if (hostModified)
{
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
Line line =
t.GetObject(hostId, OpenMode.ForRead)
as Line;
Circle circle =
t.GetObject(anchoredId, OpenMode.ForRead)
as Circle;
{
Point3d p = circle.Center;
Point3d ps = line.StartPoint;
Point3d pe = line.EndPoint;
double lineLength = ps.DistanceTo(pe);
double circleOffset = ps.DistanceTo(p);
a = circleOffset / lineLength;
}
t.Commit();
}
}
_ids.Add(anchoredId);
_pos.Add(a);
}
}
void _db_ObjectOpenedForModify(
object sender,
ObjectEventArgs e
)
{
ObjectId id = e.DBObject.Id;
if (_mapAnchoredToHost.ContainsKey(id))
{
Debug.Assert(
e.DBObject is Circle,
"Expected anchored object to be a circle"
);
saveLocation(_mapAnchoredToHost, id, false);
}
else if (_mapHostToAnchored.ContainsKey(id))
{
Debug.Assert(
e.DBObject is Line,
"Expected host object to be a line"
);
foreach (ObjectId id2 in _mapHostToAnchored)
{
saveLocation(id, id2, true);
}
}
}
void rollbackLocations()
{
Debug.Assert(
_ids.Count == _pos.Count,
"Expected same number of ids and locations"
);
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
int i = 0;
foreach (ObjectId id in _ids)
{
Circle circle =
t.GetObject(id, OpenMode.ForWrite)
as Circle;
Line line =
t.GetObject(
_mapAnchoredToHost,
OpenMode.ForRead
) as Line;
Point3d ps = line.StartPoint;
Point3d pe = line.EndPoint;
double a = _pos;
if (a.Equals(double.NaN))
{
LineSegment3d segment =
new LineSegment3d(ps, pe);
Point3d p = circle.Center;
circle.Center =
segment.GetClosestPointTo(p).Point;
}
else
{
circle.Center = ps + a * (pe - ps);
}
}
t.Commit();
}
}
}
}To see how it works, draw a circle and a line:
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Circle%20and%20line_thumb.png
Call the ANCHOR command, selecting the line and then the circle. The circle gets moved such that its centre is at the closest point on the line:
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Circle%20anchored%20to%20line_thumb.png
Now we can grip-stretch the top point on the line:
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Line%20endpoint%20being%20moved_thumb.png
And the circle snaps back onto the line:
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Circle%20and%20line%20after%20move_thumb.png
If we grip-stretch the circle away from the line...
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Move%20circle%20from%20line_thumb.png
And the circle then snaps back to be on the line, but at a different point (the closest to the one selected):
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Circle%20snaps%20back%20onto%20the%20line_thumb.png
If you're interested in other posts demonstrating the use of reactors to anchor entities, this series of posts shows how to link circles together: Linking Circles Parts 1, 2, 3, 4 & 5.
四、撤销AutoCad命令对特定实体的影响
August 13, 2008
Rolling back the effect of AutoCAD commands using .NET
Another big thank you to Jeremy Tammik, from our DevTech team in Europe, for providing this elegant sample. This is another one Jeremy presented at the recent advanced custom entity workshop in Prague. I have added some initial commentary as well as some steps to see the code working. Jeremy also provided the code for the last post.
We sometimes want to stop entities from being modified in certain ways, and there are a few different approaches possible, for instance: at the simplest - and least granular - level, we can place entities on locked layers or veto certain commands using an editor reactor.Or we can go all-out and implement custom objects that have complete control over their behaviour. The below technique provides a nice balance between control and simplicity: it makes use of a Document event to check when a particular command is being called, a Database event to cache the information we wish to restore and finally another Document event to restore it. In this case it's all about location (or should I say "location, location, location" ? :-). We're caching an object's state before the MOVE command (which changes an object's position in the model), but if we wanted to roll back the effect of other commands, we would probably want to cache other properties.
Here's the C# code:
using System.Diagnostics;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
namespace Reactor
{
/// <summary>
/// Reactor command.
///
/// Demonstrate a simple object reactor, as well as
/// cascaded event handling.
///
/// In this sample, the MOVE command is cancelled for
/// all red circles. This is achieved by attaching an
/// editor reactor and watching for the MOVE command begin.
/// When triggered, the reactor attaches an object reactor
/// to the database and watches for red circles. If any are
/// detected, their object id and original position are
/// stored. When the command ends, the positions are
/// restored and the object reactor removed again.
///
/// Reactors create overhead, so we should add them only
/// when needed and remove them as soon as possible
/// afterwards.
/// </summary>
public class CmdReactor
{
private static Document _doc;
private static ObjectIdCollection _ids =
new ObjectIdCollection();
private static Point3dCollection _pts =
new Point3dCollection();
static public void Reactor()
{
_doc =
Application.DocumentManager.MdiActiveDocument;
_doc.CommandWillStart +=
new CommandEventHandler(doc_CommandWillStart);
}
static void doc_CommandWillStart(
object sender,
CommandEventArgs e
)
{
if (e.GlobalCommandName == "MOVE")
{
_ids.Clear();
_pts.Clear();
_doc.Database.ObjectOpenedForModify +=
new ObjectEventHandler(_db_ObjectOpenedForModify);
_doc.CommandCancelled +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed +=
new CommandEventHandler(_doc_CommandEnded);
}
}
static void removeEventHandlers()
{
_doc.CommandCancelled -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed -=
new CommandEventHandler(_doc_CommandEnded);
_doc.Database.ObjectOpenedForModify -=
new ObjectEventHandler(_db_ObjectOpenedForModify);
}
static void _doc_CommandEnded(
object sender,
CommandEventArgs e
)
{
// Remove database reactor before restoring positions
removeEventHandlers();
rollbackLocations();
}
static void _db_ObjectOpenedForModify(
object sender,
ObjectEventArgs e
)
{
Circle circle = e.DBObject as Circle;
if (null != circle && 1 == circle.ColorIndex)
{
// In AutoCAD 2007, OpenedForModify is called only
// once by MOVE.
// In 2008, OpenedForModify is called multiple
// times by the MOVE command ... we are only
// interested in the first call, because
// in the second one, the object location
// has already been changed:
if (!_ids.Contains(circle.ObjectId))
{
_ids.Add(circle.ObjectId);
_pts.Add(circle.Center);
}
}
}
static void rollbackLocations()
{
Debug.Assert(
_ids.Count == _pts.Count,
"Expected same number of ids and locations"
);
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
int i = 0;
foreach (ObjectId id in _ids)
{
Circle circle =
t.GetObject(id, OpenMode.ForWrite) as Circle;
circle.Center = _pts;
}
t.Commit();
}
}
}
}To see the code at work, draw some circles and make some of them red:
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Lots%20of%20circles_thumb.png
Now run the REACTOR command and try to MOVE all the circles:
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Moving%20the%20circles_thumb.png
Although all the circles are dragged during the move, once we complete the command we can see that the red circles have remained in the same location (or have, in fact, had their location rolled back). The other circles have been moved, as expected.
http://through-the-interface.typepad.com/through_the_interface/WindowsLiveWriter/Circles%20post-move_thumb.png
五、阻止特定图块的炸开
August 18, 2008
Preventing an AutoCAD block from being exploded using .NET
In response to these recent posts, I received a comment from Nick:
By any chance would it be possible to provide an example to prevent a user from using the EXPLODE command for a given block name?
I delved into the ADN knowledgebase and came across this helpful ObjectARX DevNote, which I used to create a .NET module to address the above question.
Here's the C# code, which should contain enough comments to make it self-explanatory:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
namespace ExplosionPrevention
{
public class Commands
{
private Document _doc;
private Database _db;
private ObjectIdCollection _blkDefs =
new ObjectIdCollection();
private ObjectIdCollection _blkRefs =
new ObjectIdCollection();
private ObjectIdCollection _blkConts =
new ObjectIdCollection();
private bool _handlers = false;
private bool _exploding = false;
public void StopBlockFromExploding()
{
_doc =
Application.DocumentManager.MdiActiveDocument;
_db = _doc.Database;
if (!_handlers)
{
AddEventHandlers();
_handlers = true;
}
// Get the name of the block to protect
PromptStringOptions pso =
new PromptStringOptions(
"\nEnter block name: "
);
pso.AllowSpaces = false;
PromptResult pr =
_doc.Editor.GetString(pso);
if (pr.Status != PromptStatus.OK)
return;
Transaction tr =
_db.TransactionManager.StartTransaction();
using (tr)
{
// Make sure the block definition exists
BlockTable bt =
(BlockTable)
tr.GetObject(
_db.BlockTableId,
OpenMode.ForRead
);
if (bt.Has(pr.StringResult))
{
// Collect information about the block...
// 1. the block definition
ObjectId blkId =
bt;
_blkDefs.Add(blkId);
BlockTableRecord btr =
(BlockTableRecord)
tr.GetObject(
blkId,
OpenMode.ForRead
);
// 2. the block's contents
foreach (ObjectId id in btr)
_blkConts.Add(id);
// 3. the block's references
ObjectIdCollection blkRefs =
btr.GetBlockReferenceIds(true, true);
foreach (ObjectId id in blkRefs)
_blkRefs.Add(id);
}
tr.Commit();
}
}
private void AddEventHandlers()
{
// When a block reference is added, we need to
// check whether it's for a block we care about
// and add it to the list, if so
_db.ObjectAppended +=
delegate(object sender, ObjectEventArgs e)
{
BlockReference br =
e.DBObject as BlockReference;
if (br != null)
{
if (_blkDefs.Contains(br.BlockTableRecord))
_blkRefs.Add(br.ObjectId);
}
};
// Conversely we need to remove block references
// that as they're erased
_db.ObjectErased +=
delegate(object sender, ObjectErasedEventArgs e)
{
// This is called during as part of the cloning
// process, so let's check that's not happening
if (!_exploding)
{
BlockReference br =
e.DBObject as BlockReference;
if (br != null)
{
// If we're erasing, remove this block
// reference from the list, otherwise if
// we're unerasing we will want to add it
// back in
if (e.Erased)
{
if (_blkRefs.Contains(br.ObjectId))
_blkRefs.Remove(br.ObjectId);
}
else
{
if (_blkDefs.Contains(br.BlockTableRecord))
_blkRefs.Add(br.ObjectId);
}
}
}
};
// This is where we fool AutoCAD into thinking the
// block contents have already been cloned
_db.BeginDeepClone +=
delegate(object sender, IdMappingEventArgs e)
{
// Only for the explode context
if (e.IdMapping.DeepCloneContext !=
DeepCloneType.Explode)
return;
// We add IDs to the map to stop the
// block contents from being cloned
foreach (ObjectId id in _blkConts)
e.IdMapping.Add(
new IdPair(id, id, true, true, true)
);
};
// And this is where we remove the mapping entries
_db.BeginDeepCloneTranslation +=
delegate(object sender, IdMappingEventArgs e)
{
// Only for the explode context
if (e.IdMapping.DeepCloneContext !=
DeepCloneType.Explode)
return;
// Set the flag for our CommandEnded handler
_exploding = true;
// Remove the entries we added on BeginDeepClone
foreach (ObjectId id in _blkConts)
e.IdMapping.Delete(id);
};
// As the command ends we unerase the block references
_doc.CommandEnded +=
delegate(object sender, CommandEventArgs e)
{
if (e.GlobalCommandName == "EXPLODE" && _exploding)
{
// By this point the block contents should not have
// been cloned, but the blocks have been erased
Transaction tr =
_db.TransactionManager.StartTransaction();
using (tr)
{
// So we need to unerase each of the erased
// block references
foreach (ObjectId id in _blkRefs)
{
DBObject obj =
tr.GetObject(
id,
OpenMode.ForRead,
true
);
// Only unerase it if it's needed
if (obj.IsErased)
{
obj.UpgradeOpen();
obj.Erase(false);
}
}
tr.Commit();
}
_exploding = false;
}
};
}
}
}
The STOPEX command takes a block name and then gathers (and stores) information about a block: its ObjectId, the IDs of its contents and its various block references. I've added some logic to handle creation of new block references (e.g. via INSERT), and erasure of ones that are no longer needed. I haven't put anything in to deal with redefinition of blocks (if the contents of blocks change then explosion may not be prevented properly), but this is left as an exercise for the reader.
Let's define and insert a series of three blocks: LINES, ARCS and CIRCLES (no prizes for guessing which is which :-):
Now we run the STOPEX command on the LINES and CIRCLES blocks:
Command: STOPEX
Enter block name: circles
Command: STOPEX
Enter block name: lines
Command: EXPLODE
Select objects: all
9 found
Select objects:
Command: Specify opposite corner:Selecting the "exploded" blocks, we see that only the ARCS blocks have actually been exploded:
六、使用任务对话框[Task Dialogs]
November 10, 2008
Implementing task dialogs inside AutoCAD using .NET - Part 1
This is a topic I've been meaning to get to for some time... as I finally had to research it for a side project I'm working on, I decided to go ahead and post my findings here.
AutoCAD 2009 makes heavy use of task dialogs, which are basically message-boxes on steroids. MSDN contains documentation on Microsoft's implementation of task dialogs, although our implementation is a little different.
Why bother with these new task dialogs? They provide a way of asking more user-friendly questions using actual actions as answers rather than yes/no/cancel etc. It's a bit like the way I now often seem to be asked questions such as "who packed this bag?" when flying, these days, rather than "did you pack this bag yourself?" - you have to think a little more, but that usually increases the chances of getting accurate information. Alright, perhaps the airline security analogy wasn't all that appropriate, after all, but hopefully you get the idea.
In this first part of the two-part series we're going to look at the basic steps to add task dialogs to our projects, showing a very basic example that gives an idea of their capabilities, and in the next part we'll see a much more concrete, real-world implementation example.
The first thing to do to make use of task dialogs in your application is to add a reference to the AdWindows.dll assembly. You should find it in AutoCAD 2009 (but not previous versions), in the root application folder. As usual when adding AutoCAD's managed assemblies to your project, remember to set the "Copy Local" flag to false, to avoid problems later. Once we've added this reference we're able to use the Autodesk.Windows namespace (not to be confused with Autodesk.AutoCAD.Windows, which is AutoCAD-specific).
Here's our initial C# code to show a very basic task dialog, without doing very much with the information provided:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.Windows;
namespace TaskDialogs
{
public class Commands
{
public static void TestingTaskDialogOptions()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
// Create the task dialog itself
TaskDialog td = new TaskDialog();
// Set the various textual settings
td.WindowTitle = "The title";
td.MainInstruction = "Something has happened.";
td.ContentText =
"Here's some text, with a " +
"<A HREF=\"http://adn.autodesk.com\">" +
"link to the ADN site</A>";
td.VerificationText = "Verification text";
td.FooterText =
"The footer with a "+
"<A HREF=\"http://blogs.autodesk.com/through" +
"-the-interface\">link to Kean's blog</A>";
td.EnableHyperlinks = true;
td.EnableVerificationHandler = true;
// And those for collapsed/expanded text
td.CollapsedControlText =
"This control text can be expanded.";
td.ExpandedControlText
= "This control text has been expanded..." +
"\nTo span multiple lines.";
td.ExpandedText = "This footer text has been expanded.";
td.ExpandFooterArea = true;
td.ExpandedByDefault = false;
// Set some standard icons and display of the progress bar
td.MainIcon = TaskDialogIcon.Shield;
td.FooterIcon = TaskDialogIcon.Information;
td.ShowProgressBar = true;
// A marquee progress bas just loops,
// it has no rangefixed upfront
//td.ShowMarqueeProgressBar = true;
// Now we add out task action buttons
td.UseCommandLinks = true;
td.Buttons.Add(
new TaskDialogButton(
1,
"This is one course of action."
)
);
td.Buttons.Add(
new TaskDialogButton(
2,
"Here is another course of action."
)
);
td.Buttons.Add(
new TaskDialogButton(
3,
"And would you believe we have a third!"
)
);
// Set the default to be the third
td.DefaultButton = 3;
// And some radio buttons, too
td.RadioButtons.Add(new TaskDialogButton(4, "Yes"));
td.RadioButtons.Add(new TaskDialogButton(5, "No"));
td.RadioButtons.Add(new TaskDialogButton(6, "Maybe"));
// Set the default to be the second
td.DefaultRadioButton = 5;
// Allow the dialog to be cancelled
td.AllowDialogCancellation = false;
// Implement a callback for UI event notification
td.Callback =
delegate(
ActiveTaskDialog atd,
TaskDialogEventArgs e,
object sender
)
{
ed.WriteMessage(
"\nButton ID: {0}",
e.ButtonId
);
ed.WriteMessage(
"\nNotification: {0}",
e.Notification
);
if (e.Notification ==
TaskDialogNotification.VerificationClicked)
{
atd.SetProgressBarRange(0, 100);
atd.SetProgressBarPosition(80);
}
else if (e.Notification ==
TaskDialogNotification.HyperlinkClicked)
{
ed.WriteMessage(" " + e.Hyperlink);
}
ed.WriteMessage("\n");
// Returning true will prevent the dialog from
// being closed
return false;
};
td.Show(Application.MainWindow.Handle);
}
}
}
When we execute the TASK1 command, we can see this dialog gets displayed:
This dialog exercises most of the UI options available in the TaskDialog class. You'll see a progress bar (which gets modified via the ActiveTaskDialog passed into the notification callback - try checking the "Verification text" box to see that happen) as well as option buttons, action buttons and some expandable text.
Here's how the dialog looks with the text expanded and the verification checkbox ticked:
It's worth playing around with the code to see how the notification is provided to the application, and how the ActiveTaskDialog can be used from there to modify the state of the dialog. One thing you'll notice is that the hyperlinks don't automatically result in a browser being launched, which overall is a good thing, as the links can then be set up to result in other actions (the application has all the information it needs to know what was clicked in the notification callback - if it wants to launch a browser to the clicked URL, it is free to do so).
Now that we've explored the various options open to us, in the next post we'll look at a more real-world example of using a task dialog to help manage potentially time-consuming application behaviour.
November 14, 2008
Implementing task dialogs inside AutoCAD using .NET - Part 2
In this previous post we looked at a basic task dialog inside AutoCAD and exercised its various capabilities without showing how they might be used in a real application. This post goes beyond that to show how you might make use of the TaskDialog class to provide your users with relevant information at runtime that helps them decide how best to proceed in certain situations, effectively increasing your application's usability.
The specific scenario is this: if the user selects a lot of entities - too many for our command to handle quickly - we want to show a dialog that allows the user to decide whether to proceed anyway, to select fewer entities or to cancel completely from the command. For the sake of argument we're going to set a threshold of 1,000 entities being "a lot", although it may well be that for your various operations 100 or 100,000 is a better number).
Here's some C# code that asks the user to select entities and then either performs an operation on them or presents a number of options to the user:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.Windows;
namespace TaskDialogs
{
public class Commands
{
// In a real application we would retrieve this
// persistent setting from somewhere
static bool neveragain = false;
enum DecisionOptions
{
DoOperation,
Reselect,
Cancel
};
public static void MoreRealisticTask()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
bool done = false;
while (!done)
{
PromptSelectionResult psr = ed.GetSelection();
if (psr.Status != PromptStatus.OK)
done = true;
else
{
DecisionOptions decision = DecisionOptions.DoOperation;
if (psr.Value.Count > 1000 && !neveragain)
{
TaskDialog td = new TaskDialog();
td.Width = 200;
td.WindowTitle =
"Many objects selected";
td.MainInstruction =
psr.Value.Count.ToString() +
" objects have been selected, " +
"which may lead to a time-consuming operation.";
td.UseCommandLinks = true;
td.Buttons.Add(
new TaskDialogButton(
0,
"Perform the operation on the selected objects."
)
);
td.Buttons.Add(
new TaskDialogButton(
1,
"Reselect objects to edit"
)
);
td.Buttons.Add(
new TaskDialogButton(
2,
"Do nothing and cancel the command"
)
);
td.VerificationText =
"Do not ask me this again";
td.Callback =
delegate(
ActiveTaskDialog atd,
TaskDialogEventArgs e,
object sender
)
{
if (e.Notification ==
TaskDialogNotification.ButtonClicked)
{
switch (e.ButtonId)
{
case 0:
decision = DecisionOptions.DoOperation;
break;
case 1:
decision = DecisionOptions.Reselect;
break;
case 2:
decision = DecisionOptions.Cancel;
break;
}
}
if (e.Notification ==
TaskDialogNotification.VerificationClicked)
{
neveragain = true;
}
return false;
};
td.Show(Application.MainWindow.Handle);
}
switch (decision)
{
case DecisionOptions.DoOperation:
// Perform the operation anyway...
ed.WriteMessage(
"\nThis is where we do something " +
"with our {0} entities.",
psr.Value.Count
);
done = true;
break;
case DecisionOptions.Reselect:
done = false;
break;
// Includes DecisionOptions.Cancel...
default:
done = true;
break;
}
}
}
// This is where we would store the value of neveragain
// in our persistent application settings
}
}
}Here's what happens when we run the TASK2 command and select 2,500 entities:
The behaviour should be pretty predictable - you can reselect again and again, until you either:
Go ahead and perform the operation (action 1)
Reselect fewer than the threshold of 1000 entities (action 2)
Give up and cancel the command (action 3)
Decide not to be asked the question again (the checkbox at the bottom)
If you select the checkbox and reselect (action 2), for instance, you won't then be asked the question again during that session, even if you go ahead and select 100,000 entities. One thing the application doesn't do is to save the value of this setting - you would need to store it somewhere in your application settings (probably in the Registry or a configuration file of some kind), and ideally provide the user with some ability to re-enable the task dialog, should they decide it's a good idea to have it, after all.
七、在选项对话框中加入自定义项目
November 19, 2008
Adding a custom tab to AutoCAD's options dialog using .NET - Part 1
A big thanks to Viru Aithal, from our DevTech India team, for providing the code that inspired this post.
Update: it turns out I didn't look deeply enough into the origins of the code behind this post. The code that inspired Viru's code that inspired mine came from our old friend Mike Tuersley, who's delivering a class on customizing the Options dialog at this year's AU (in just over a week). Thanks, Mike! :-)
One way that applications often want to integrate with AutoCAD is via the dialog displayed by the OPTIONS command. Luckily it's relatively easy to add your own custom tab to this dialog using .NET, allowing users to view and modify your application's settings. I'm going to take a similar approach to this topic as I did with task dialogs: part 1 (this post) focuses on a very simple implementation to show the basic capabilities of the mechanism, and part 2 will look at a more "real world" implementation that goes as far as providing access to and storing some actual (well, realistic, at least) application settings.
The first step when implementing your custom tab is to add a User Control to your project. As this activity goes beyond just copy & pasting code, here's a sample project demonstrating the technique shown in this post.
Once you have a User Control in your project (ours has the default name UserControl1), you can design it to add in some custom controls - whether buttons, checkboxes, combos, or more complex controls such as a property grid (something I'll show in the next post). Here's the design I settled for, containing just a few, simple controls, each added with its default name:
You can clearly adjust the layout, as you wish, with anchor points, docking, etc. It's worth setting the "modifier" for each of the controls you've added to public or internal, assuming you want to access their state directly from elsewhere in the project. The other way is to expose them via public properties, but that's really up to you.
Here's the code that goes behind this dialog, the important activity being to set the "dirty" flag for the control when a particular component control is selected:
using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
namespace OptionsDlg
{
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
private void button1_Click(
object sender,
EventArgs e
)
{
TabbedDialogExtension.SetDirty(this, true);
}
private void checkBox1_CheckedChanged(
object sender,
EventArgs e
)
{
TabbedDialogExtension.SetDirty(this, true);
}
private void comboBox1_SelectedIndexChanged(
object sender,
EventArgs e
)
{
TabbedDialogExtension.SetDirty(this, true);
}
}
}
Now for our rest of the implementation. We're going to add our custom tab on load of the application (see these two previous posts for the approach for running code on application load), so there's no need to implement a command. Here's the C# code:
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
[assembly:
ExtensionApplication(
typeof(OneNeedsOptions.Initialization)
)
]
namespace OneNeedsOptions
{
class Initialization : IExtensionApplication
{
static OptionsDlg.UserControl1 _uc;
public void Initialize()
{
Application.DisplayingOptionDialog +=
new TabbedDialogEventHandler(
Application_DisplayingOptionDialog
);
}
public void Terminate()
{
Application.DisplayingOptionDialog -=
new TabbedDialogEventHandler(
Application_DisplayingOptionDialog
);
}
private static void ButtonPressedMessage(string name)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(name + " button pressed.\n");
}
private static void ShowSettings()
{
if (_uc != null)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(
"Settings were: checkbox contains {0}," +
" combobox contains {1}\n",
_uc.checkBox1.Checked,
_uc.comboBox1.Text
);
}
}
private static void OnOK()
{
ButtonPressedMessage("OK");
ShowSettings();
}
private static void OnCancel()
{
ButtonPressedMessage("Cancel");
}
private static void OnHelp()
{
ButtonPressedMessage("Help");
}
private static void OnApply()
{
ButtonPressedMessage("Apply");
ShowSettings();
}
private static void Application_DisplayingOptionDialog(
object sender,
TabbedDialogEventArgs e
)
{
if (_uc == null)
{
_uc = new OptionsDlg.UserControl1();
TabbedDialogExtension tde =
new TabbedDialogExtension(
_uc,
new TabbedDialogAction(OnOK),
new TabbedDialogAction(OnCancel),
new TabbedDialogAction(OnHelp),
new TabbedDialogAction(OnApply)
);
e.AddTab("My Custom Settings", tde);
}
}
}
}
When we build the application, load it into AutoCAD and run the OPTIONS command, we should see our custom settings appear in our new tab:
As the various settings in the dialog get modified (even the button being clicked), you'll see the "Apply" button get enabled - a direct result of the settings on the control being considered "dirty". As you select the various buttons belonging to the dialog you should see messages on the command-line, as the respective events get fired.
In the next post we'll extend this example to provide access to persistent properties, as you would want to do from a real application.
页:
[1]
2