AutoCAD.NET二次开发注意事项与开发技巧
1、如果要在Winform中打开DWG图形文件,这个Form必须用Application.ShowModelessDialog方式显示,不然会报错(执行环境无效)。
2、在非模态窗口中修改实体时,需要将图层锁定,否则会报错;模态窗口则无此情况。
3、写块克隆:WblockCloneObjects第二个参数为块表记录模型空间的ObjectID,此方法会将一个对象集(实体集)完全克隆到另一个DWG文件中,包括图层名、图层设置、坐标位置、扩展数据等。
4、在CAD中用后台打开方式打开DWG图形文件,并进行修改后,要用SaveAs保存,用Save会出错。
5、使用模态窗口时,如果需要与CAD主窗体进行交互,则用using (EditorUserInteraction edUI = ed.StartUserInteraction(this));使用非模态窗口时,会出现焦点切换问题 (如:当在非模态窗口中点击按钮后要去CAD中选择一个实体,但是应用程序的焦点还在非模态窗口中,此时需要在CAD主窗口中点击一下让CAD获取焦点,才能正常进行实体选取),此时可以用WinAPI中的SetFocus将焦点移到CAD主窗口即可:
[DllImport("user32.dll", EntryPoint = "SetFocus")] public static extern int SetFocus(IntPtr hWnd);
调用:
SetFocus(Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument.Window.Handle);
要注意的是:需要配合窗体的MouseEnter、MouseLeave事件使用,但效果并不太理想,如果鼠标移动较快的时候,事件来不及触发。暂时还没有找到其他更好的方法,C++中可以接收一个KeepFocus消息,来监视和设置程序焦点,使焦点转换更灵活,C#中也可以收到CAD发来的这个消息,但是这个消息的Msg值不是固定的,C++中使用的时候不需要管这个值,因为是由CAD已经提供了,所以可以准确地方监听这个消息;但是在C#中CAD并没有提供这个Msg值,所以并不能使用监听Windows消息的方法来实现焦点的切换功能。
6、AutoCAD.NET获取CAD当前图层代码:
Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument.Database.Clayer;
7、AutoCAD 注册表项在32位与64系统中的不同
AutoCAD注册表信息读取,如获取电脑上已安装的所有CAD版本、安装路径等注册表信息。在32位Windows系统中,这些信息保存在:HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\AutoCAD 中;而64位Windows系统中,32位的CAD软件信息保存在 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Autodesk\AutoCAD 中,64位CAD软件信息保存在 HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\AutoCAD 中。
在64位系统中,安装的CAD版本可能是32位,也可能是64位。如果在32位的CAD中调用我们的DLL,此DLL读取 HKEY_LOCAL_MACHINE\SOFTWARE 时,实际读到的是 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node;如果在64位的CAD中调用我们的DLL,则此时DLL读取 HKEY_LOCAL_MACHINE\SOFTWARE 时,会得到真正的HKEY_LOCAL_MACHINE\SOFTWARE 对象。
上述情况是因为,为了做到64位系统兼容32位程序,同时又为了防止注册表项冲突,所以32位应用程序在64位Windows系统中操作 HKEY_LOCAL_MACHINE\SOFTWARE 键时会被自动转到 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node。(具体说明可见:http://www.cnblogs.com/mingmingruyuedlut/archive/2011/01/20/1940371.html)
这种问题的解决方法参考:
较通用的WINAPI方法
http://www.cnblogs.com/mingmingruyuedlut/archive/2011/01/21/1941225.html
适用.NET 4.0的新方法:
http://www.cnblogs.com/langu/archive/2012/02/26/2368877.html
但是还有一点要注意:在CAD二次开发中,由于我们的DLL最终是要被CAD调用的,所以我们程序集的位数是由CAD的位数来决定的。
8、修改命令快捷键并立即生效
在CAD中点击:工具-》自定义-》编辑程序参数(acad.pgp),在打开的文件中找到“Command alias format”,在其下方可以看到命令别名定义列表,定义的格式为:“别名,*命令”,每行只能定义一个,定义完成之后,重启CAD即可生效。如果不想重启CAD,还有两种方式可以让你的定义生效:
1)、在命令文本中输入reinit,回车,在弹窗中选中“PGP文件”,然后确定,如下图:
2)、在命令文本中输入re-init,回车,然后输入16,再回车即可。
第二种方法应该可以在程序中进行实现,不知道AutoCAD.NET中有没有提供现成的方法,不过可以用sendcommand实现。这样可以为用户提供快捷键的自定义功能,并且在用户定义完之后,可以立即生效。acad.pgp文件位置在:CAD安装目录\UserDataCache\Support中,是一个隐藏文件。
9、eInvalidOpenState错误问题
当在事务A中调用GetObject方法获取了对象E,在事务A结束之后,如果在另一个事务B中调用E的UpgradeOpen时,会出现错误提示:eInvalidOpenState,这时候需要在事务B中用GetObject根据E.ObjectId重新获取E才行。
10、eWasNotOpenForWrite错误问题
同问题情况类似,在事务A中调用GetObject方法获取了对象E,在事务A结束之后,如果在另一个事务B中调用E的DowngradeOpen方法,则会出现错误提示:eWasNotOpenForWrite,这时候需要在事务B中用GetObject根据E.ObjectId重新获取E。
11、SelectionFilter 根据XData扩展数据过滤选择集问题
使用根据对象的扩展数据XData进行过滤选择时,如果过滤条件仅为DxfCode.ExtendedDataRegAppName(即注册的应用程序名)时,可以过滤所有类型的对象,但是如果要用DxfCode.ExtendedDataAsciiString、DxfCode.ExtendedDataInteger16等具体数据内容进行过滤时,则仅能选取到部分类型的对象,如:Line、PolyLine、Ellipse、Region,而像DBText、MText、Circle等类型的实体则会被忽略掉。
12、SelectionFilter过滤选择单行文本对象问题
用SelectionFilter过滤选择文本时,切记单行文本必须写成TypedValue((int)DxfCode.Start,"Text"),要写成"Text",而不是"DBText"。
13、用Hatch填充时的问题
用Hatch填充Region时,如果面域对象是由多个面域合并而来,会填充失败,此时可以调用Region对象的Explode方法将Region炸散,得到多个Region,然后将这些Region一一填充。
对于Hatch.AppendLoop第二个参数:ObjectIdCollection 类型,传入的是一个或多个实体对象的ID,这些实体必须满足以下几个条件:
1)、如果只传入一个对象,那么这个对象必须是一个非“回”字型闭合对象,如闭合的pline、单个的Region、Circle、Ellipse等;如果传入的对象是回字型,即中间是空心的,那么将会报错,填充将失败。
2)、如果传入的是多个对象,那么这些对象必须正好可以首尾相接地组成一个闭合图形,且没有富余的对象,这样才能成功填充;如果组成的图形不能闭合,或者闭合后还剩余一些无用处的线、弧等,又或者组成了多个闭合图形,那么将会报错,填充将失败。
14、嵌套事务问题
使用嵌套事务时,好像如果外层的事务不提交,即使里面的事务提交了,最终也会失效,不知道这种说法对不对,因为今天我在里层的事务里创建了一个Hatch并且提交了事务,在代码里看Hatch已经完全创建成功了,但是外层的事务没有提交,结果执行结束后在CAD中完全找不到Hatch的影子。
15、Autodesk.AutoCAD.DatabaseServices.DataTable 扩展数据表使用时要注意
当对象实体的扩展字典中还没有这个表时,DBDictionary.SetAt 和 Transaction.AddNewlyCreatedDBObject 将扩展数据表添加到实体对象,但是修改时就不能再这样了,只需要获取DataTable对象,然后用 DataTable.SetCellAt 方法修改数据表的值,最后提交事务即可。(注:向实体扩展字典中添加DataTable时,必须将DataTable添加到BlockTableRecord中,不然在保存CAD图纸的时候,会保存失败)。
16、PaletteSet停靠。
PaletteSet可以用程序控制其最小尺寸,停靠位置,可停靠位置,按钮、菜单显示等,但是用程序指定其停靠位置时,每个面板只能占据单独一列或一行,不能做到让多个面板上下同列多行停靠,或左右同行多列停靠。
PaletteSet有两个重载函数,一般都用第一个,即在声明PaletteSet时只传一个name参数。第二个重载有两个参数,一个是name参数,一个是guid参数,CAD会以这个guid为标识,记录此面板的位置、大小等信息,这样在下一次,加载这个面板的时候,如果不指定面板的Dock属性,则会自动使用上次保存的设置来管理面板的停靠位置和大小等。即:第一次在CAD里加载此面板时,可以给面板指定一个默认的停靠位置,此时用户可以随意拖动面板重新指定停靠位置,当用户关闭CAD时,这个面板的停靠设置会被CAD保存下来,下一次加载的时候,如果不再给此面板指定Dock属性,则会自动使用上次保存的设置信息进行停靠,要实现这一点,最好用PaletteSet的Add和Save事件进行配合(具体实现,请看这篇文章)。
但是,这种实现方式,会有一个问题:如果在声明PaletteSet时指定了传了name参数,CAD会自动创建一个和这个name一样的启动命令,就像每次打开CAD时的“COMMANDLINE”命令一样,每次CAD启动时都会执行,但是我们创建PaletteSet的方法并不一定会声明成为一个命令,而且就算声明成命令,这个命令也不一定和PaletteSet的name参数一样,这样在CAD启动时,就会提示“未知命令”,令人很不爽,解决方法有三种:
1、将创建PaletteSet的方法声明为一个命令,且命令要和PaletteSet的name参数保持一致,而且你的dll要随CAD自动启动(可以用注册表方法实现),但是这种方法太过狭隘,并不好。
2、在声明PaletteSet的时候,name参数只传一个空字符串,PaletteSet实例化之后,再为PaletteSet指定Text属性指定面板的名称即可,这样CAD启动时,就不会自动执行命令了。
3、CAD保存面板停靠信息时,是保存在特殊文件夹Application Data中的,即C#中的Environment.SpecialFolder.ApplicationData文件夹,比如我在XP系统中用32位CAD 2008测试,保存路径为:C:\Documents and Settings\Administrator\Application Data\Autodesk\AutoCAD 2008\R17.1\chs\Support\Profiles\FixedProfile.aws,这是一个xml文件。
如果声明PaletteSet的时候指定了第二个guid参数,CAD会在FixedProfile.aws文件ToolsInfo节点中添加一个Tool节点,用来保存面板信息,同时会在StartupInfo中也添加一个Tool节点,指定启动命令,这个启动命令和PaletteSet的name参数一样,就算在声明PaletteSet的时候传的是一个空字符串,CAD也会在StartupInfo节点下添加一个Command=""的启动命令,CAD启动时,应该是会把空命令忽略,所以使用第二个解决方法,CAD在启动时不会执行面板对应的启动命令,如果你对此有强迫症,不希望CAD保存启动命令节点的话,那么你可以自行将其删除,方法如下:
在声明PaletteSet的时候,可以随意指定name属性,或干脆留空,这个不再有影响;实现CAD提供的IExtensionApplication接口,此的接口中的Terminate会在你的DLL被卸载时(即CAD被关闭时)执行,我们可以在这个方法内,打开当前CAD对应的FixedProfile.aws文件,然后把StartupInfo节点下面板对应的启动命令删除,此命令和之前声明PaletteSet的时候指定的name参数一样,删除之后,保存FixedProfile.aws文件即可。
17、Application.ShowModelessDialog在不同版本CAD的调用不同
在CAD2010之前,这个方法有三个重载:
public static void ShowModelessDialog(Form formToShow); public static void ShowModelessDialog(IWin32Window owner, Form formToShow); public static void ShowModelessDialog(IWin32Window owner, Form formToShow, bool persistSizeAndPosition);
而在CAD2010中,这个方法有五个重载:
public static void ShowModelessDialog(Form formToShow); public static void ShowModelessDialog(IntPtr owner, Form formToShow); public static void ShowModelessDialog(IWin32Window owner, Form formToShow); public static void ShowModelessDialog(IntPtr owner, Form formToShow, bool persistSizeAndPosition); public static void ShowModelessDialog(IWin32Window owner, Form formToShow, bool persistSizeAndPosition);
然而,在CAD2010中,第三和第五个重载并没有什么用,因为CAD2010的窗体对象(Application.MainWindow,即Autodesk.AutoCAD.Windows.Window类)不再实现IWin32Window接口,所以第一个参数没办法传了,只能使用第一、二、四个重载方法。这是我目前的研究结果,不知道第三、第五个重载方法在CAD2010中还有没有其他使用方式。
另外,上面的ShowModelessDialog是针对Winform窗体的,而CAD2010的窗体却都是WPF的,于是在CAD2010中又提供了一个新接口方法:ShowModelessWindow,这个方法也是有五个重载:
public static void ShowModelessWindow(System.Windows.Window formToShow); public static void ShowModelessWindow(IntPtr owner, System.Windows.Window formToShow); public static void ShowModelessWindow(System.Windows.Window owner, System.Windows.Window formToShow); public static void ShowModelessWindow(IntPtr owner, System.Windows.Window formToShow, bool persistSizeAndPosition); public static void ShowModelessWindow(System.Windows.Window owner, System.Windows.Window formToShow, bool persistSizeAndPosition);
这个接口方法是用来显示WPF窗体对象的,但是,虽然有五个重载方法,但是应该和ShowModelessDialog一样,只有第一、二、四个方法能正常使用。
18、不允许所请求的注册表访问权。在win7等64位操作系统中,在CAD调用的C# dll插件中,如果操作注册表(读取、写入等),可能会报这个错误,原因是CAD不是以管理员权限运行的,权限不足,要防止这种情况发生,需要让CAD以管理员身份运行,在CAD的快捷方式或安装目录下的acad.exe上右键,选择“属性”,切换到“兼容性”页面,选中“以管理员身份运行此程序”,这样就不会出现这种情况了。
另外,对于这种错误,网上还有一种解决方案,是在操作注册表的方法上加上RegistryPermissionAttribute属性标识,如[RegistryPermissionAttribute(SecurityAction.PermitOnly, Read = @"HKEY_LOCAL_MACHINE\SOFTWARE\YourApp")],但是这种情况在CAD开发上好像并不适用,会提示错误。
19、在使用各种实体对象之后(如DBObject、Entity、Region、Polyline、DBObjectCollection等),尽量主动调用其Dispose方法进行释放,否则当你频繁对一组实体进行重复操作时,可能会引发一些错误,甚至引起CAD的崩溃。比如我碰到的一种情况:http://www.cnblogs.com/bomb12138/p/4552507.html这里面的第2条。
20、如果为面域指定了实体颜色,在此面域和其他面域进行了布尔运算之后(面域相加、相减、合并),面域的颜色会变成ByLayer 。
21、用注册表实现C#DLL或C++ARX随CAD自动加载,可以往以下两个地方添加启动信息:
1)、HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\RXX.X\ACAD-XXXX:XXX\Applications
2)、64位系统中的64位CAD 或 32位系统中的CAD:HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\AutoCAD\RXX.X\ACAD-XXXX:XXX\Applications
64位系统中的32位CAD:HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Autodesk\AutoCAD\RXX.X\ACAD-XXXX:XXX\Applications
其中,Current_User项不分32位和64位,而LOCAL_Machine则会分32位和64位,用程序进行注册表读写时需要注意。但是CURRENT_USER注册表信息只对当前登录的系统用户有效,而LOCAL_MACHINE注册表信息则会对系统所有用户都有效。
C#DLL或C++ARX随CAD启动需要向Applications项中添加一个自己命名的项,然后在该项下添加以下几个值:
DESCRIPTION | 字符串 | 程序名称或描述 |
LOADCTRLS | DWord |
启动类型(十进制值): 2=随CAD启动加载 4=有命令触发时加载 8=有载入请求时加载 16=从不加载该应用程序 32=显式加载该应该程序 |
LOADER | 字符串 | 要加载的文件位置 |
MANAGED | DWord | 如果是C#DLL,十进制为1,如果是ARX则不添加此项 |
LOADCTRLS的值可以单独使用,也可以多个值相加使用,例如:12=4+8=有命令触发时加载或有载入请求时加载。
另外,如果要使用类型4(有命令触发时加载),除了设置以上几个值之外,还需要在自己创建的应用程序节点下创建新项并注册命令,这样在CAD启动时,不会直接加载DLL或ARX,当注册的命令被触发时,会加载DLL或ARX:
在自己的应用程序节点下添加“Commands”项,在此项中添加String类型的值,这些值就是注册的命令,例如:
图中右侧的Fcmd就是我们注册的命令,当启动CAD后,MyAP所指向的程序并不会被直接加载,只有当Fcmd命令被使用的时候,CAD才会加载MyAP程序。
关于命令注册这个,在官方AutoCAD.NET文档和常见的二次开发书籍中都没有提到,还是无意间从这里找到的。
22、用过滤选择进行polyine的时候,类型名必须写成LWPolyline,而要过滤选择polyline2d对象时,类型名要写成polyline。
23、关于Document.CommandEnded事件:正常来说自己定义的命令都应该可以触发CommandEnded事件,但如果你的命令中使用了Application.ShowModelDialog,那么你的命令将不会触发CommandEnded事件,如果把Application.ShowModelDialog换成Form本身的ShowDialog,则可以正常触发CommandEnded事件。
24、使用VS2010 + AutoCAD2008进行开发时,如果不能调试,可以尝试修改AutoCAD2008安装目录中的acad.exe.config文件:
<configuration> <startup> <!--We always use the latest version of the framework installed on the computer. If you are having problems then explicitly specify .NET 2.0 by uncommenting the following line. <supportedRuntime version="v2.0.50727"/> --> <supportedRuntime version="v2.0.50727" /> </startup> </configuration>
25、使用SendStringToExecute向CAD发送命令时,如果命令参数中带有路径,这时如果执行有问题,可以尝试使用“/”代替路径中的“\”。
26、关于在C#中使用P/Invoke方式调用CAD的acedCmd或acedCommand方法发送同步命令,如果这两个接口函数是在自己的自定义命令中调用的,那么可以成功执行,并且是同步的,但如果是在自己的某个窗体程序里调用的,那么可能会执行不成功(接口返回-5001)或者执行成功,但成了异步命令。另外,CAD2013以下版本这两个接口是在acad.exe中的,而从CAD2013开始放到了accore.dll中。
另外,在CAD中还有一个未公开接口也可以发送同步命令:acedPostCommand,这个命令同样在CAD2013以下版本中是在acad.exe中,从CAD2013开始放到了accore.dll中,另外还有一点不同的是:在CAD2008中它的入口点是“?acedPostCommand@@YAHPB_W@Z”,而从CAD2009开始,它的入口点名称变成了“?acedPostCommand@@YAHPEB_W@Z”,这一点使用的时候需要注意。
27、AutoCAD2010在打开新图纸时,按ESC键,可能会影响到DocumentManager_DocumentActivated中 CAD操作的执行,具体情况是:
有一个程序为DocumentManager绑定了DocumentActivated事件,在这个事件中进行图纸扫描,扫描大部分的图形实体。
在打开测试图纸时,会有一个弹窗:
此时如何按下ESC来关闭此窗体,那么在窗体关闭之后由于图纸窗口又获取到了焦点,所以会触发DocumentActivated事件,但是之前按的ESC键会影响事件中扫描程序的执行,使程序报错:eUserBreak。
这个情况只在AutoCAD2010中会出现,在其他版本CAD中测试未发现这种情况,至于在CAD2010中,用了一个简单方法解决了这个问题:创建一个窗体,在创建中放一个定时器,把扫描程序放到定时器中执行,再定义一个公共方法启动定时器,这样在DocumentActivated事件中调用此窗体的公共方法启动定时器进行图纸扫描,这样不不需要弹出窗体就可以完成工作。除了这个方法使用线程或者直接声明定时器也许也能解决这个问题,不过没有进行尝试,不知道行不行得通,等有时间试过之后再更新结果。
28、当加载的多个DLL或ARX中有同名的自定义命令时,最后在使用时触发的是最后加载的那个程序集中的命令。
29、如果要创建匿名块(在CAD的块编译器中看不到,但是在BlockTable中可以查找到并可以添加引用显示到图形中,并且不可以手动编辑),只需要在创建BlockTableRecord对象的时候指定其Name为“*U”即可,当把这个BlockTableRecord被成功添加到BlockTable后,会自动会重新分配一个名称,格式为“*U数字”。
30、关于Database_ObjectModified事件:如果启动了一个事务,且最后没有提交它,那么在这个事务中的所有更改都不生效,且不会触发Database_ObjectModified事件。如果最后提交了这个事务,那么如果在打开对象时是以OpenMode.ForWrite方式打开的,那么不管到底是否对这个对象有更改都会触发Database_ObjectModified事件;而如果在打开对象时是使用的OpenMode.ForRead打开的,后面没有对实体进行修改则不会触发Database_ObjectModified事件,如果后面使用UpgradeOpen升级了读写方式不管到底有没有对实体做出修改,也都会触发Database_ObjectModified事件。
另外,在此事件内部使用事务时要注意以下两点,否则会弹出崩溃提示:Error handler re-entered;Exiting now
1.此事件内不能使用嵌套事务。
2.并且此事件外有其他事务正在被使用时不能再打开新事务。
31、实体的IntersectWith方法,当参与运算的实体的顶点越多耗时越长,两个实体,当调用此方法的主实体是顶点数较少的那个实体,则耗时会较短,当调用此方法的主实体是顶点数较多的那个实体,则耗时会较长(使用Stopwatch的ElapsedTicks检测)。
32、在使用Explode接口将实体炸开到一个DBObjectCollection中之后,发现不能直接按从尾到头的顺序去遍历DBObjectCollection,否则会报错(大致意思是说索引超出边界或索引不能为负值),但事实上索引值肯定是没有错的。后来发现,如果将使用正向对DBObjectCollection进行一次遍历,或者按照正向顺序取出DBObjectCollection的前两三个元素(例如DBObject dbo1=dbos[0];DBObject dbo2=dbos[1];先用正向索引读取一下集合的前几个元素),然后再使用倒序方式对DBObjectCollection进行遍历,就不会再提示错误了,这个问题感觉有些不可思议。(在AutoCAD2008中发现的问题,其他版本没有测试)
33、使用VS调试AutoCAD 2012-2014版本时,如果出现命中不了断点,且提示“无可用源”,那么可以尝试查看AutoCAD的FIBERWORLD变量的值是否为1,如果是1则可以使用NEXTFIBERWORLD命令将其修改为0,修改完成之后再进行调试即可正常命中断点。(详情可见这里)