I am new-ish to C# programming (and programming in general) but I'm getting my feet wet with AutoCAD development using the AutoDesk .NET API for projects at work.
There are certain repetitive tasks in AutoCAD dev that I've been creating helper methods for to simplify my code. In order to create an object(lines, polylines, annotation, etc...) in AutoCAD through the .API, the programmer has to write a fairly convoluted statement that accesses the AutoCAD environment, gets the current drawing, gets the database of the current drawing file, starts a transaction with the database, //do work, then append the created entities to the database before finally committing and closing the transaction.
So I wrote the following code to simplify this task:
public static void CreateObjectActionWithinTransaction(Action<Transaction, Database, BlockTable, BlockTableRecord> action)
{
var document = Application.DocumentManager.MdiActiveDocument;
var database = document.Database;
using (var transaction = document.TransactionManager.StartTransaction())
{
BlockTable blocktable = transaction.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord blockTableRecord = transaction.GetObject(blocktable[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;
action(transaction, database, blocktable, blockTableRecord);
transaction.Commit();
}
}
Then my Lambda expression that creates a generic MText and sets up some parameters for it:
public static void createMtext(Point3d location, AttachmentPoint attachmentpoint, string contents, double height, short color, bool usebackgroundmask, bool usebackgroundcolor, double backgroundscale)
{
CreateObjectActionWithinTransaction((transaction, database, blocktable, blocktablerecord) =>
{
MText mt = new MText();
mt.SetDatabaseDefaults();
mt.Location = location;
mt.Attachment = attachmentpoint;
mt.Contents = contents;
mt.Height = height;
mt.Color = Color.FromColorIndex(ColorMethod.ByAci, color);
mt.BackgroundFill = usebackgroundmask;
mt.UseBackgroundColor = usebackgroundcolor;
mt.BackgroundScaleFactor = backgroundscale;
blocktablerecord.AppendEntity(mt);
transaction.AddNewlyCreatedDBObject(mt, true);
});
}
And then finally, when I'm actually creating MText somewhere, I can create it in one line, and pass in values for all the parameters without having to write out the huge transaction code for it:
Helpers.createMtext(insertpoint, AttachmentPoint.MiddleLeft, "hello world", .08, colors.AutoCAD_Red, true, true, 1.2);
So this is great and it works when I want to create an MText by itself and put it somewhere. However, there are certain other situations where instead of just creating an MText and placing it in the drawing, I want to create an MText using the same basic premise as above, but return it as a value to be used somewhere else.
AutoCAD has annotation objects called Multileaders which are essentially just an MText just like above, but attached to some lines and an arrow to point at something in the drawing. In the API you need to define an MText and attach it to the Multileader object. However my above code can't be used because it doesn't return anything.
So my question boils down to, how can I create a method like above to create an object, but instead of just creating that object, have it return that object to be used by another piece of code?
Also are there any good resources for beginners on Lambda expressions? Books, websites, YouTube?
Instead of using delegates, I'd rather use extension methods called from within a transaction in the calling method.
static class ExtensionMethods
{
public static BlockTableRecord GetModelSpace(this Database db, OpenMode mode = OpenMode.ForRead)
{
var tr = db.TransactionManager.TopTransaction;
if (tr == null)
throw new Autodesk.AutoCAD.Runtime.Exception(ErrorStatus.NoActiveTransactions);
return (BlockTableRecord)tr.GetObject(SymbolUtilityServices.GetBlockModelSpaceId(db), mode);
}
public static void Add(this BlockTableRecord btr, Entity entity)
{
var tr = btr.Database.TransactionManager.TopTransaction;
if (tr == null)
throw new Autodesk.AutoCAD.Runtime.Exception(ErrorStatus.NoActiveTransactions);
btr.AppendEntity(entity);
tr.AddNewlyCreatedDBObject(entity, true);
}
public static MText AddMtext(this BlockTableRecord btr,
Point3d location,
AttachmentPoint attachmentpoint,
string contents,
double height,
short color = 256,
bool usebackgroundmask = false,
bool usebackgroundcolor = false,
double backgroundscale = 1.5)
{
MText mt = new MText();
mt.SetDatabaseDefaults();
mt.Location = location;
mt.Attachment = attachmentpoint;
mt.Contents = contents;
mt.Height = height;
mt.ColorIndex = color;
mt.BackgroundFill = usebackgroundmask;
mt.UseBackgroundColor = usebackgroundcolor;
mt.BackgroundScaleFactor = backgroundscale;
btr.Add(mt);
return mt;
}
}
Using example:
public static void Test()
{
var doc = AcAp.DocumentManager.MdiActiveDocument;
var db = doc.Database;
using (var tr = db.TransactionManager.StartTransaction())
{
var ms = db.GetModelSpace(OpenMode.ForWrite);
var mt = ms.AddMtext(Point3d.Origin, AttachmentPoint.TopLeft, "foobar", 2.5);
// do what you want with 'mt'
tr.Commit();
}
}
For the AutoCAD part:
As Miiir stated in the comment, do not return an object, but rather ObjectId. An object instance belongs to a transaction, so if you open the object using some transaction, commit the transaction and try and use that object in another transaction, AutoCAD will basically just crash.
Working with AutoCAD API always follows this basic pattern:
Start transaction
Create new object or use transaction to get an existing object. This is done by either having an ObjectID or by looping over tables and looking for whatever attributes you are interested in (i.e. BlockTable, BlockTableRecord, LayerTable, etc.)
Do stuff to the object.
Commit or Abort transaction.
If you try and bypass steps 1 and 2, it will not work out so well. So, return ObjectID, and then use the id to get the object in another transaction.
As for the C# part:
If you are looking to return a value using a delegate, Action<T> is not your friend. Action does not return a value, it only "acts", thus the name. If you want to use a delegate to return a value you have 2 options:
Define a custom delegate type.
Use the generic delegate supplied by the .NET framework Func<T1,T2,T3,T4,TResult>.
Which one should you use? In your case, I'd probably go with option 1, for the simple reason that your code will just be much cleaner as easier to maintain. I will use that in this example. Using Func would work the exact same way, except your function signatures would look a bit ugly.
Custom delegate:
//somewhere in your code inside a namespace (rather than a class)
public delegate ObjectId MyCreateDelegate(Transaction transaction, Database db,
BlockTable blockTable, BlockTableRecord blockTableRecord);
Then your general method
public static ObjectId CreateObjectActionWithinTransaction(MyCreateDelegate createDel)
{
ObjectId ret;
var document = Application.DocumentManager.MdiActiveDocument;
var database = document.Database;
using (var transaction = document.TransactionManager.StartTransaction())
{
BlockTable blocktable = transaction.GetObject(database.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord blockTableRecord = transaction.GetObject(blocktable[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;
//here createMtext will get called in this case, and return ObjectID
ret = createDel(transaction, database, blocktable, blockTableRecord);
transaction.Commit();
}
return ret;
}
and the specific method with the lambda:
public ObjectId createMtext(Point3d location, AttachmentPoint attachmentpoint, string contents, double height, short color, bool usebackgroundmask, bool usebackgroundcolor, double backgroundscale)
{
//here you can return the result the general function
return CreateObjectActionWithinTransaction((transaction, database, blocktable, blocktablerecord) =>
{
MText mt = new MText();
mt.SetDatabaseDefaults();
mt.Location = location;
mt.Attachment = attachmentpoint;
mt.Contents = contents;
mt.Height = height;
mt.Color = Color.FromColorIndex(ColorMethod.ByAci, color);
mt.BackgroundFill = usebackgroundmask;
mt.UseBackgroundColor = usebackgroundcolor;
mt.BackgroundScaleFactor = backgroundscale;
blocktablerecord.AppendEntity(mt);
transaction.AddNewlyCreatedDBObject(mt, true);
//make sure to get ObjectId only AFTER adding to db.
return mt.ObjectId;
});
}
And lastly, use it like this
ObjectId mtId = Helpers.createMtext(insertpoint, AttachmentPoint.MiddleLeft, "hello world", .08, colors.AutoCAD_Red, true, true, 1.2);
//now use another transaction to open the object and do stuff to it.
Learning Resources:
And lastly, to understand lambda expressions, you need to start with understanding delegates, if you don't already. All lambdas are is syntactic sugar for instantiating a delegate object that either points to a method or an anonymous method as you've done in your example. This tutorial looks pretty good. And remember, delegates such as Action, Func and Predicate, or no different. So whether you define your own delegate or use the out-of-the box solution, lambda expressions do not care.
For a lambda overview, check out this tutorial.
Do not limit yourself to the two source I provided. Just Google it, and the top 10 hits will all be fairly good information. You can also check out Pluralsight. I do a lot of my learning there.
I am not familiar with AutoCad API, but it appears that "transaction.Commit()" is the line that actually performs the action of placing the MText on your model.
if this is the case; i would do something like the following:
public MText CreateMTextObject({parameters})
{
//code
Return textObject
}
public PlaceTextObject({parameters})
{
CreateTextObject(parameters).Commit()
}
This way, you can choose to keep the textobject for further manipulation, while still allowing the option to apply it in one go. It will also have only one codeblock for making the object, making sure that there are no differences in implementation between the two methods
Have a requirement to add/remove from a CAD drawing using the .NET API an item drawn as an individual CAD entity in the Title Block area which is not an attribute. Is there any way using point references on a CAD drawing or any other method that this could be done through the API?
The CAD entity been asked to remove has an image, client address and web address.
Use this for accessing attributes
BlockTableRecord blkTblRecTitle =
transaction.GetObject(blockTable[BlockTableRecord.PaperSpace],
OpenMode.ForWrite) as BlockTableRecord;
foreach (ObjectId id in blkTblRecTitle)
{
DBObject obj = transaction.GetObject(id, OpenMode.ForWrite);
BlockReference blkRef = obj as BlockReference;
if (blkRef != null)
{
AttributeCollection attCol = blkRef.AttributeCollection;
foreach (ObjectId objID in attCol)
{
DBObject dbObj = transaction.GetObject(objID,
OpenMode.ForWrite) as DBObject;
AttributeReference acAttRef = dbObj as
AttributeReference;
}
}
}
Have managed to find how to add multi line text at a location
using (MText acText = new MText())
{
acText.Location = new Point3d(802, 106.5, 0);
acText.TextHeight = 2;
acText.Contents = "Hello World.\nNow need to right align text.";
blkTblRecTitle.AppendEntity(acText);
transaction.AddNewlyCreatedDBObject(acText, true);
}
Ideally would like to right align but can't see anywhere how I do this with MText which have to use for multiple lines.
Given problems adding text to Paper Space can't see how ever be able to add an image.
To edit .DWG files you need AutoCAD engine, which can be:
desktop: using the existing AutoCAD on your machine, create a .NET/VBA/LISP/C++ plugin that will open, read, modify and save the file. Here is a .NET tutorial.
cloud: using Forge Design Automation webservice to run custom commands/routines with your files. Check the documentation.
Then I would suggest reviewing the blogs here and here for sample codes.
I am writing a C#.NET program that interacts with AutoCAD through the AutoCAD .NET API. The program loops through DWG files in a directory and checks every text entity on the "testLayer" layer to see if it matches "testText". I got this to work by opening up every file and making a Selectionfilter to get all of the entities on the "testLayer" layer.
Application.DocumentManager.Open(curfile.FullName, false);
....
Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
using (Transaction acTrans = doc.TransactionManager.StartTransaction())
{
ObjectIdCollection ents = new ObjectIdCollection();
// Set up filter and filter on layer name
TypedValue[] tvs = new TypedValue[1] { new TypedValue((int)DxfCode.LayerName, "testLayer")};
SelectionFilter sf = new SelectionFilter(tvs);
PromptSelectionResult psr = ed.SelectAll(sf);
if (psr.Status == PromptStatus.OK)
{
// Get the object ids for all of the entities for the filtered layer
ents = new ObjectIdCollection(psr.Value.GetObjectIds());
foreach (ObjectId objid in ents)
{
DBText dbText = acTrans.GetObject(objid, OpenMode.ForRead) as DBText;
if (dbText.TextString.Contains("testText")
{
return dbText.TextString;
}
}
return "";
}
else
{
return "";
}
}
}
But now I am converting my program to side-load the underlying databases because it was taking too long for the program to open and close every .DWG file. The problem is that now I am using
db.ReadDwgFile(currentDWG, FileOpenMode.OpenForReadAndAllShare, true, string.Empty);
to read files without actually opening them so I can't use
Editor ed = Application.DocumentManager.MdiActiveDocument.Editor
and
ed.SelectAll(sf) for the selection filter strategy I was using earlier because the document isn't actually open. So how can I can get all of the text entities on each layer named "testLayer" without actually opening the DWG file?
In a 'side database', to mimic SelectAll, you have to iterate through all entities in all the layouts and check the entity layer.
EDIT: In a 'side database', to mimic SelectAll, you have to iterate through all entities in all the layouts and check the entity type and layer.
private IEnumerable<ObjectId> GetTextEntitiesOnLayer(Database db, string layerName)
{
using (var tr = db.TransactionManager.StartOpenCloseTransaction())
{
var blockTable = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
foreach (ObjectId btrId in blockTable)
{
var btr = (BlockTableRecord)tr.GetObject(btrId, OpenMode.ForRead);
var textClass = RXObject.GetClass(typeof(DBText));
if (btr.IsLayout)
{
foreach (ObjectId id in btr)
{
if (id.ObjectClass == textClass)
{
var text = (DBText)tr.GetObject(id, OpenMode.ForRead);
if (text.Layer.Equals(layerName, System.StringComparison.CurrentCultureIgnoreCase))
{
yield return id;
}
}
}
}
}
}
}
I'm trying to write a method that prompts the user to select all the entities they want to combine into a block and then joins them together into a block and returns the block reference. Right now it looks like this.
/// <summary>
/// Returns all entities in an AutoCAD drawing in a list
/// </summary>
public static List<Entity> GetEntitiesInDrawing()
{
List<Entity> entitiesToReturn = new List<Entity>(); //Blocks that will be returned
Transaction tr = _database.TransactionManager.StartTransaction();
DocumentLock docLock = _activeDocument.LockDocument();
using (tr)
using (docLock)
{
BlockTableRecord blockTableRecord = (BlockTableRecord)tr.GetObject(SymbolUtilityServices.GetBlockModelSpaceId(_database), OpenMode.ForRead);
foreach (ObjectId id in blockTableRecord)
{
try
{
Entity ent = (Entity)tr.GetObject(id, OpenMode.ForWrite);
entitiesToReturn.Add(ent);
}
catch (InvalidCastException)
{
continue;
}
}
}
return entitiesToReturn;
}
/// <summary>
/// Prompts the user for a number of entities and then joins them into a block
/// </summary>
public static BlockReference JoinEntities()
{
BlockReference blkToReturn = null;
List<Entity> entitiesToJoin = PromptUserForEntities();
foreach (Entity ent in entitiesToJoin)
{
// ToDo: Join entities into blkToReturn
}
return blkToReturn;
}
My problem is that I have no idea how or if it is possible to take a list of entities and join them into a blockreference.
Kean covered this in his blog: Creating an AutoCAD block using .NET
In summary:
use Editor.Getselection so the user can select the entities
create a blockTableRecord (BTR) on the BlockTable (from
Database.BlockTableId)
append all entities to the newly created BTR, here you may need to create new entities or move ownership (see BlockTableRecord.AssumeOwnershipOf method)
create a new blockreference that points to the BTR
open the Model Space (or Paper Space) and append the block reference to it
optional: erase all original entities from the model space (avoid
duplicated), if you haven't changed ownership
The post mentioned can help, but it creates new entities (and doesn't move from model to the block definition (step #3)
I want to display entities on a drawing area as a preview for the user, then if the user accepts the program, add the the entities to the database or make some modification.
I'm used to use transaction and commit the transaction the entities appear if i can make the entities appear before commit the transaction
using (Transaction tr = db.TransactionManager.StartTransaction())
{
BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;
int i = poly2TxtSetting.currentFormat.IndexFormat.startWith;
List<ObjectId> ListTextId = new List<ObjectId>();
List<ObjectId> ListPointId = new List<ObjectId>();
foreach (var po in Points)
{
i += poly2TxtSetting.currentFormat.IndexFormat.step;
DBText dtext = new DBText();
dtext.TextString = i.tostring();
dtext.Position = po;
dtext.SetDatabaseDefaults();
DBPoint point = new DBPoint(po);
btr.AppendEntity(dtext);
tr.AddNewlyCreatedDBObject(dtext, true);
btr.AppendEntity(point);
tr.AddNewlyCreatedDBObject(point, true);
}
tr.Commit();
}
If you want to display your model in AutoCAD model space, you have two options.
1) Insert it into database.
2) Add it into Transient Manager.
I think you need is 2nd option.
Search for Transient Graphics.
Check below Code that will help you.
Solid3d solid=new Solid(0);
solid.CreateSphere(10);
TransientManager.CurrentTransientManager.AddTransient(solid, TransientDrawingMode.Main, 128, new IntegerCollection());
This will display sphere on origin with radius=10;
You can wait for the graphics flush:
tr.TransactionManager.QueueForGraphicsFlush();
then prompt for input so the user has time to see the update:
PromptKeywordOptions pko = new PromptKeywordOptions("\nKeep Changes?");
pko.AllowNone = true;
pko.Keywords.Add("Y");
pko.Keywords.Add("N");
pko.Keywords.Default = "Y";
PromptResult pkr = ed.GetKeywords(pko);
if (pkr.StringResult == "Y") {
tr.Commit();
} else {
tr.Abort();
}
This link provides an example application using this technique.