一 Calling AutoCAD commands from .NET
使用.NET调用AutoCAD命令
In this earlier entry I showed some techniques for calling AutoCAD commands programmatically from ObjectARX and from VB(A). Thanks to Scott Underwood for proposing that I also mention calling commands `me via IM this evening with the C# P/Invoke declarations for ads_queueexpr() and acedPostCommand(). It felt like the planets were aligned... :-)
在早期的帖子中展示过以ObjectARX和VB编程的方式实现调用AutoCAD的内置命令,感谢Scott Underwood建议我今晚也提及一下利用PInvoke的方式调用ads_queueexpr和acedPostCommand
Here are some ways to send commands to AutoCAD from a .NET app:
四中方式
- SendStringToExecute from the managed document object(.NET)
- SendCommand from the COM document object(COM)
- acedPostCommand via P/Invoke(ARX)
- ads_queueexpr() via P/Invoke(ADS)
Here's some VB.NET code - you'll need to add in a COM reference to AutoCAD 2007 Type Library in addition to the standard .NET references to acmgd.dll and acdbmgd.dll.
Vb.net代码展示,需要引用COM库,.net基类
Imports Autodesk.AutoCAD.Runtime Imports Autodesk.AutoCAD.ApplicationServices Imports Autodesk.AutoCAD.Interop Public Class SendCommandTest Private Declare Auto Function ads_queueexpr Lib "acad.exe" _ (ByVal strExpr As String) As Integer Private Declare Auto Function acedPostCommand Lib "acad.exe" _ Alias "?acedPostCommand@@YAHPB_W@Z" _ (ByVal strExpr As String) As Integer <CommandMethod("TEST1")> _ Public Sub SendStringToExecuteTest() Dim doc As Autodesk.AutoCAD.ApplicationServices.Document doc = Application.DocumentManager.MdiActiveDocument doc.SendStringToExecute("_POINT 1,1,0 ", False, False, True) End Sub <CommandMethod("TEST2")> _ Public Sub SendCommandTest() Dim app As AcadApplication = Application.AcadApplication app.ActiveDocument.SendCommand("_POINT 2,2,0 ") End Sub <CommandMethod("TEST3")> _ Public Sub PostCommandTest() acedPostCommand("_POINT 3,3,0 ") End Sub <CommandMethod("TEST4")> _ Public Sub QueueExprTest() ads_queueexpr("(command""_POINT"" ""4,4,0"")") End Sub End Class In case you're working in C#, here are the declarations of acedPostCommand() and ads_queueexpr() that Jorge sent to me: [DllImport("acad.exe", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] extern static private int ads_queueexpr(string strExpr); [DllImport("acad.exe", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "?acedPostCommand@@YAHPB_W@Z")] extern static private int acedPostCommand(string strExpr);
You'll need to specify:
using System.Runtime.InteropServices;
to get DllImport to work, of course.
二 Import blocks from an external DWG file using .NET
We're going to use a "side database" - a drawing that is loaded in memory, but not into the AutoCAD editor - to import the blocks from another drawing into the one active in the editor.
意思:不打开外部的dwg向图形空间导入实体
Here's some C# code. The inline comments describe what is being done along the way. Incidentally, the code could very easily be converted into a RealDWG application that works outside of AutoCAD (we would simply need to change the destDb from the MdiActiveDocument's Database to the HostApplicationServices' WorkingDatabase, and use a different user interface for getting/presenting strings from/to the user).
我们要使用一个辅助数据库“side database”――一个被载入内存,但不在AutoCAD编辑器的显示的图纸,使用这个辅助数据库来从其它的图纸中导入块到编辑器中的活动图纸。 下面是关于此的C#代码,其中的注释描述了每个步骤。顺便说一句,下面的代码很容易被转换成一个RealDWG程序,这个程序工作于AutoCAD外部(只需要简单地把目标数据库(destDb)从MdiActiveDocument的数据库改变成HostApplicationService的WorkingDatabase,并使用不同的用户接口来为用户得到和表现字符串)
using System; using Autodesk.AutoCAD; using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using System.Collections.Generic; namespace BlockImport { public class BlockImportClass { [CommandMethod("IB")] public void ImportBlocks() { DocumentCollection dm = Application.DocumentManager; Editor ed = dm.MdiActiveDocument.Editor; Database destDb = dm.MdiActiveDocument.Database; Database sourceDb = new Database(false, true); PromptResult sourceFileName; try { // Get name of DWG from which to copy blocks sourceFileName = ed.GetString("\nEnter the name of the source drawing: "); // Read the DWG into a side database sourceDb.ReadDwgFile(sourceFileName.StringResult, System.IO.FileShare.Read, true, ""); // Create a variable to store the list of block identifiers ObjectIdCollection blockIds = new ObjectIdCollection(); Autodesk.AutoCAD.DatabaseServices.TransactionManager tm = sourceDb.TransactionManager; using (Transaction myT = tm.StartTransaction()) { // Open the block table BlockTable bt = (BlockTable)tm.GetObject(sourceDb.BlockTableId, OpenMode.ForRead, false); // Check each block in the block table foreach (ObjectId btrId in bt) { BlockTableRecord btr = (BlockTableRecord)tm.GetObject(btrId, OpenMode.ForRead, false); // Only add named & non-layout blocks to the copy list if (!btr.IsAnonymous && !btr.IsLayout) blockIds.Add(btrId); btr.Dispose(); } } // Copy blocks from source to destination database IdMapping mapping = new IdMapping(); sourceDb.WblockCloneObjects(blockIds, destDb.BlockTableId, mapping, DuplicateRecordCloning.Replace, false); ed.WriteMessage("\nCopied " + blockIds.Count.ToString() + " block definitions from " + sourceFileName.StringResult + " to the current drawing."); } catch(Autodesk.AutoCAD.Runtime.Exception ex) { ed.WriteMessage("\nError during copy: " + ex.Message); } sourceDb.Dispose(); } } }
And that's all there is to it. More information on the various objects/properties/methods used can be found in the ObjectARX Reference.
三 Breaking it down - a closer look at the C# code for importing blocks
I didn't spend as much time as would have liked talking about the code in the previous topic(it was getting late on Friday night when I posted it). Here is a breakdown of the important function calls.
The first major thing we do in the code is to declare and instantiate a new Database object. This is the object that will represent our in-memory drawing (our side database). The information in this drawing will be accessible to us, but not loaded in AutoCAD's editor.
Database sourceDb = new Database(false, true);
Very importantly, the first argument (buildDefaultDrawing) is false. You will only ever need to set this to true in two situations. If you happen to pass in true by mistake when not needed, the function will return without an error, but the DWG will almost certainly be corrupt. This comes up quite regularly, so you really need to watch for this subtle issue.
Here are the two cases where you will want to set buildDefaultDrawing to true:
- When you intend to create the drawing yourself, and not read it in from somewhere
- When you intend to read in a drawing that was created in R12 or before
Although this particular sample doesn't show the technique, if you expect to be reading pre-R13 DWGs into your application in a side database, you will need to check the DWG's version and then pass in the appropriate value into the Database constructor. Here you may very well ask, "but how do I check a DWG's version before I read it in?" Luckily, the first 6 bytes of any DWG indicate its version number (just load a DWG into Notepad and check out the initial characters):
AC1.50 = R2.05
AC1002 = R2.6
AC1004 = R9
AC1006 = R10
AC1009 = R11/R12
AC1012 = R13
AC1014 = R14
AC1015 = 2000/2000i/2002
AC1018 = 2004/2005/2006
AC1021 = 2007
You'll be able to use the file access routines of your chosen programming environment to read the first 6 characters in - AC1009 or below will require a first argument of true, otherwise you're fine with false.
Next we ask the user for the path & filename of the DWG file:
sourceFileName = ed.GetString("\nEnter the name of the source drawing: ");
Nothing very interesting here, other than the fact I've chosen not to check whether the file actually exists (or even whether the user entered anything). The reason is simple enough: the next function call (to ReadDwgFile()) will throw an exception if the file doesn't exist, and the try-catch block will pick this up and report it to the user. We could, for example, check for that particular failure and print a more elegant message than "Error during copy: eFileNotFound", but frankly that's just cosmetic - the exception is caught and handled well enough.
sourceDb.ReadDwgFile(sourceFileName.StringResult, System.IO.FileShare.Read, true, "");
This is the function call that reads in our drawing into the side database. We pass in the results of the GetString() call into the filename argument, specifying we're just reading the file (for the purposes of file-locking: this simply means that other applications will be able to read the DWG at the same time as ours but not write to it). We then specify that we wish AutoCAD to attempt silent conversion of DWGs using a code-page (a pre-Unicode concept related to localized text) that is different to the one used by the OS we're running on. The last argument specifies a blank password (we're assuming the drawing being opened is either not password protected or its password has already been entered into the session's password cache).
Next we instantiate a collection object to store the IDs of all the blocks we wish to copy across from the side database to the active one:
ObjectIdCollection blockIds = new ObjectIdCollection();
We then create a transaction which will allow us to access interesting parts of the DWG (this is the recommended way to access DWG content in .NET). Using the transaction we open the block table of the side database for read access, specifying that we only wish to access it if it has not been erased:
BlockTable bt = (BlockTable)tm.GetObject(sourceDb.BlockTableId, OpenMode.ForRead, false);
From here - and this is one of the beauties of using the managed API to AutoCAD - we simply use a standard foreach loop to check each of the block definitions (or "block table records" in AcDb parlance).
foreach (ObjectId btrId in bt) { BlockTableRecord btr = (BlockTableRecord)tm.GetObject(btrId, OpenMode.ForRead, false); // Only add named & non-layout blocks to the copy list if (!btr.IsAnonymous && !btr.IsLayout) blockIds.Add(btrId); btr.Dispose(); }
This code simply opens each block definition and only adds its ID to the list to copy if it is neither anonymous nor a layout (modelspace and each of the paperspaces are stored in DWGs as block definitions - we do not want to copy them across). We also call Dispose() on each block definition once we're done (this is a very good habit to get into).
And finally, here's the function call that does the real work:
sourceDb.WblockCloneObjects(blockIds, destDb.BlockTableId, mapping, DuplicateRecordCloning.Replace, false);
WblockCloneObjects() takes a list of objects and attempts to clone them across databases - we specify the owner to be the block table of the target database, and that should any of the blocks we're copying (i.e. their names) already exist in the target database, then they should be overwritten ("Replace"). You could also specify that the copy should not happen for these pre-existing blocks ("Ignore").
四 Supporting multiple AutoCAD versions - conditional compilation in C#
简要介绍一下吧,就不翻译啦,意思就是在命令注册前添加一段[Conditional("AC2007")]这个特性,就可以实现在版本升级的时候,屏蔽函数的变化,减少代码重构。
Someone asked by email how to get the block import code first shown last week and further discussed in yesterday's post working with AutoCAD 2006. I've been using AutoCAD 2007 for this, but to get it working there are only a few changes to be made.
Firstly you'll need to make sure you have the correct versions of acmgd.dll and acdbmgd.dll referenced into the project (you should be able to tell from their path which version of AutoCAD they were installed with).
Secondly you'll need to modify your code slightly, as WblockCloneObjects() has changed its signature from 2006 to 2007. The below code segment shows how to use pre-processor directives to implement conditional compilation in C#. You will need to set either AC2006 or AC2007 as a "conditional compilation symbol" in your project settings for this to work:
#if AC2006 mapping = sourceDb.WblockCloneObjects(blockIds, destDb.BlockTableId, DuplicateRecordCloning.Replace, false); #elif AC2007 sourceDb.WblockCloneObjects(blockIds, destDb.BlockTableId, mapping, DuplicateRecordCloning.Replace, false); #endif
You would possibly use #else rather than #elif above, simply because that would make your code more future-proof: it would automatically support new versions without needing to specifically add code to check for them.
On a side note, you can actually specify that entire methods should only be compiled into a certain version. Firstly, you'll need to import the System.Diagnostics namespace:
using System.Diagnostics;
Then you simply use the Conditional attribute to declare a particular method (in this case a command) for a specific version of AutoCAD (you might also specify a debug-only command by being conditional on the DEBUG pre-processor symbol, should you wish):
namespace BlockImport { public class BlockImportClass { [Conditional("AC2007"),CommandMethod("IB")] public void ImportBlock() { ...