Prehistory
I am a novice developer, “school” level of knowledge of C ++, a small (2 years) programming experience in C #, zero experience in autocade
Recently they asked me to change the LISP autocad programs for creating / modifying land-bound plans and preparing the corresponding MS Word / XML documents - fix the bugs and add new functionality.
Since the readability of Lisp programs (at least for me) leaves much to be desired, I decided to rewrite it into a more understandable language.
Because I didn’t need a millisecond increase in speed, I missed C ++ and stopped at C #
I am writing this article to:
1. Arrange in my head on the shelves that I learned about Autocad.
2. Help those who, like me, break through a very small amount of documentation.
3. Get in the comments information like "you do it wrong, it will be easier and better to do so ..."
')
Beginning of work. Creating a plugin.
Create a C # project using the ClassLibrary template
Add links to the Autocad API libraries managed library, which are in the program folder.
In my case it is:
C: \ Program Files \ AutoCAD 2007 \ acdbmgd.dll
C: \ Program Files \ AutoCAD 2007 \ acmgd.dll
Create a class that does something:using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.ApplicationServices; namespace AutocadPlugin { public class test : IExtensionApplication { [CommandMethod("hello")] public void Helloworld() { var editor = Application.DocumentManager.MdiActiveDocument.Editor; editor.WriteMessage(" Autocad "); } public void Initialize() { var editor = Application.DocumentManager.MdiActiveDocument.Editor; editor.WriteMessage(" .."+Environment.NewLine); } public void Terminate() { } } }
Inheritance from IExtensionApplication is optional, Autocad will automatically hook all public classes into libraries, but, as I was told, it will be faster. Plus you can control the Initialize / Terminate plugin.
We compile, launch the autoCAD, load the plugin with the netload command (the managed dll selection window opens)
Now when you enter the hello command, we will receive the expected response.
The structure of the Autocad application:
What we see on the screen are graphical objects inherited from
EntityIn addition to the visible, there are invisible information objects -
Layers ,
Line Types ,
Dimension Styles, Table Styles , etc.
All this is stored in the
Database Table Records , in storages of the type
TYPETable and classes of the type
TYPETableRecord .
Object IDs
- ObjectId , also known as EName (entity name). The number created when opening a picture.
Use - identification of the object in the database within one session, the object is requested from the base by its ObjectId
May vary between different discoveries, it is better not to use to save references to objects.
- Handle - a number that does not change between different openings of a document, it is convenient to use to save links between objects when saving a file.
For more information on how to access a drawing, it can be used. In the database there is only a while the database is loaded into memory. Once the database is closed, the object has been assigned.Work with database
Usually work with the database is through a transaction. Objects are queried from the database, modified, and commit transactions are saved back.
During the transaction, the object is requested from the database in one of the 3 modes ForRead, ForWrite, ForNotify.
The purpose of the first two is obvious, the third is somehow used for the mechanism of events, with which I have not yet intersected
In the ForWrite mode, an autocad creates additional objects that allow you to undo the changes in the transaction.
If you need to change an object opened as “ForRead”, its UpgradeOpen () method is called.
If you call this method on an object that is already open in change mode, the method will throw an exception.
An example of getting a Polyline object by its ObjectId // UPDATE: , . , UpgradeOpen , .. public static Polyline GetPolylineByEname(ObjectId ename) { if (ename == ObjectId.Null) return null; Polyline polyline = null; var db = Application.DocumentManager.MdiActiveDocument.Database; using (var transaction = db.TransactionManager.StartTransaction()) { return transaction.GetObject(ename, OpenMode.ForRead) as Polyline; } }
Transactions can be nested, if you cancel a high-level transaction, all children are canceled.
In the beginning, for a long time I could not make out where the error was - the changes in the drawing were not saved. As it turned out, I forgot to close the upper tranz.
Illustration public void OwnerTransFunction() { var db = Application.DocumentManager.MdiActiveDocument.Database; using (var transaction = db.TransactionManager.StartTransaction()) { // ChangePolylineInChildTransation() } } public void ChangePolyline() { var db = Application.DocumentManager.MdiActiveDocument.Database; using (var transaction = db.TransactionManager.StartTransaction()) { // // transaction.Commit(); // , Commit'a } }
UPDATE: As I was told in the comments, it is preferable to always call transaction.Commit (), unless you need to cancel the transaction. If the transaction does not commit, transaction.Abort () is automatically called, incurring additional costs.
Dictionaries
I used dictionaries to save my data in DWG, so as not to create unnecessary files
I ran into two kinds of dictionaries in a drawing -
NamedObjectDictionary and
ExtensionDictionaryData in dictionaries is stored in records (Record), which in turn store typed values.
Data is sent by text keys.
NamedObjectDictionary - global drawing dictionary. It is created automatically when creating a document.
I used it to store references to the main objects I used.
ExtensionDictionary - a dictionary, its own for each object, you need to create it manually.
You can check its existence by comparing the entity.ExtensionDictionary field with ObjectId.Null
Example of writing and retrieving a string value from ExtensionDictionary public static void SetExtDictionaryValueString(ObjectId ename, string key, string value) { if (ename == ObjectId.Null) throw new ArgumentNullException("ename"); if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key"); var doc = Application.DocumentManager.MdiActiveDocument; using (var transaction = doc.Database.TransactionManager.StartTransaction()) { var entity = transaction.GetObject(ename, OpenMode.ForWrite); if (entity == null) throw new DataException(" ExtensionDictionary: entity ObjectId=" + ename + " "); // extDictionary var extensionDictionaryId = entity.ExtensionDictionary; if (extensionDictionaryId == ObjectId.Null) { entity.CreateExtensionDictionary(); extensionDictionaryId = entity.ExtensionDictionary; } var extDictionary = (DBDictionary) transaction.GetObject(extensionDictionaryId, OpenMode.ForWrite); // if (String.IsNullOrEmpty(value)) { if (extDictionary.Contains(key)) extDictionary.Remove(key); return; } var xrec = new Xrecord(); xrec.Data = new ResultBuffer(new TypedValue((int) DxfCode.ExtendedDataAsciiString, value)); extDictionary.SetAt(key, xrec); transaction.AddNewlyCreatedDBObject(xrec, true); Debug.WriteLine(entity.Handle+"['" + key + "'] = '" + value + "'"); transaction.Commit(); } } public static string GetExtDictionaryValueString(ObjectId ename, string key) { if (ename == ObjectId.Null) throw new ArgumentNullException("ename"); if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key"); var doc = Application.DocumentManager.MdiActiveDocument; using (var transaction = doc.Database.TransactionManager.StartTransaction()) { var entity = transaction.GetObject(ename, OpenMode.ForRead); if (entity == null) throw new DataException(" ExtensionDictionary: ObjectId=" + ename + " "); var extDictionaryId = entity.ExtensionDictionary; if (extDictionaryId == ObjectId.Null) throw new DataException(" ExtensionDictionary: "); var extDic = (DBDictionary)transaction.GetObject(extDictionaryId, OpenMode.ForRead); if (!extDic.Contains(key)) return null; var myDataId = extDic.GetAt(key); var readBack = (Xrecord)transaction.GetObject(myDataId, OpenMode.ForRead); return (string)readBack.Data.AsArray()[0].Value; } }
Working with the global dictionary is almost the same, only the DBDictionary object is obtained as follows:
var dictionary = (DBDictionary) transaction.GetObject(db.NamedObjectsDictionaryId, OpenMode.ForWrite);
What else have I encountered
1. Autoload pluginAfter a couple of dozen times entering the netload for a five-second verification of the plug-in, I got bored and I began to look for how to simplify the procedure.
During the search, I came across an article about autoloading .NET plug-ins. In short - you need to add the key to the registry
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\AutoCAD\R17.0\ACAD-5001:419\Applications\GeocomplexPlugin] "LOADCTRLS"=dword:00000002 "MANAGED"=dword:00000001 "LOADER"="C:\\GeoComplexAutocadPlugin\\AutocadGeocomplexPlugin.dll"
Explanations:R17.0 - Autocad 2007
419 for the Russian version
409 for the English version
GeocomplexPlugin - created section
LOADCTRLS = 2 - load auto-launch at start. Certain keys can be run on demand, the plugin is loaded when one of its commands is entered
LOADER - the path to the plugin
2. DebugBecause In the plugin, you cannot start step-by-step debugging in VS, I had to display debug messages at some stages.
Since it’s inconvenient to use the editor of an auto-cadre for these purposes, I output messages using standard tools Debug.WriteMessage ()
Debug will be displayed only when compiling in debug mode, the output data can be viewed by running the DebugView program.UPDATE: Solved the problem of step by step debugging:
He defeated this problem in this way:
In the autocad configuration file, acad.exe.config installed the runtime on v4.0:
<configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0"/> </startup> <runtime> <generatePublisherEvidence enabled="false"/> </runtime> </configuration>
Changed the execution environment of the plugin to 4.0 client profile
And AssemblyInfo.cs assembly settings commented out the attribute AllowPartiallyTrustedCallers
3. Sending a command to the EditorSome actions are not implemented in the Autocad .NET API, or they are much easier to do with the command line.
The easiest way to execute a command is to execute a function, an example for ZoomExtents:
var doc = Application.DocumentManager.MdiActiveDocument; doc.SendStringToExecute("_zoom _e ",false,false,true);
In autocade, the space is equivalent to Enter, so if you send a command without a trailing space, as a result, _e will be entered in the editor, and he will wait for further input
However, this method is not always possible to use. The principle of the SendStringToExecute command is such that the command is sent only after the function, called by the command, completes. Therefore, if you first call this command, and then, for example, prompt the user to select an object in the figure, two lines "_zoom", "_e" will be sent to the selection function, which it will perceive as incorrect objects.
We have to look for analogues that run immediately. In this case:
object acad = Application.AcadApplication; acad.GetType().InvokeMember("ZoomExtents", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, acad, null);
4. Selection of objects by the userTo select objects, use the functions of the editor'a Get *
For example, the choice of several objects - GetSelection, the choice of one object GetEntity ...
var editor = Application.DocumentManager.MdiActiveDocument.Editor; var promtResult = editor.GetEntity(" "); editor.WriteMessage(Environment.NewLine); if (promtResult.Status == PromptStatus.OK) { editor.WriteMessage("Selected Object's ID: " + promtResult.ObjectId+Enviropment.NewLine); }
Getting the path to the document folderAn Internet search gave two ways to access the full file name:
MdiActiveDocument.Database.Filename
MdiActiveDocument.Name
At first glance, they are the same, but
On the topic - I do not recommend using the property Database.Filename. After autosave, it points not to the file itself, but to the autosave copy - unlike Document.Name
I got the path to the folder using standard .NET tools:
Path.GetDirectoryName (Application.DocumentManager.MdiActiveDocument.Name);
if the returned string is empty, then the document is created but not saved
And more useful code snippets:
Changing coordinates of a polyline (extension method) // ( ): public static void UpdatePoints(this Polyline polyline, List<Point2d> newPoints) { if (polyline == null) throw new ArgumentNullException("polyline"); if (newPoints.Count < 2) throw new ArgumentException(" "); using (var transaction = Application.DocumentManager.MdiActiveDocument.Database.TransactionManager.StartTransaction()) { // , var pline = transaction.GetObject(polyline.ObjectId,OpenMode.ForWrite) as Polyline; if (pline == null) throw new DataException("! "); var bulge = pline.GetBulgeAt(0); var start_width = pline.GetStartWidthAt(0); var end_width = pline.GetEndWidthAt(0); var prevPointsCount = pline.NumberOfVertices; // // , , .. Autocad 0 1 // for (int i = prevPointsCount; i < prevPointsCount + newPoints.Count; i++) pline.AddVertexAt(i, newPoints[i - prevPointsCount], bulge, start_width, end_width); // for (int i = prevPointsCount - 1; i >= 0; i
Getting the coordinates of a polyline (extension method): // ( ): public static Point2d[] GetPoints(this Polyline polyline) { if (polyline == null) { Debug.WriteLine(" ! "); return null; } if (polyline.NumberOfVertices == 0) return null; var points = new List<Point2d>(); for (int i = 0; i < polyline.NumberOfVertices; i++) points.Add(polyline.GetPoint2dAt(i)); return points.ToArray(); }
useful links
1.
AutoCAD .NET Developer's GuideEnglish mini-reference, describes the essence of the device Autocad with code examples on VB.NET, C # .NET, VBA
2.
Through the interfaceKean Walmsley's blog, a lot of Howto. There are examples in C #, C # + COM, VB.NET, C ++. It’s impossible to search me there, but half of my requests in Google “how to do this ..” led to this site
3.
caduser.ru, subforum ".NET"Communication with Russian-speaking people well versed in the subject. Many times they helped me in difficult places.