对AutoCAD的二次开发是采用插件的方式,即运行AutoCAD.net API编写dll文件,运行时在AutoCAD命令行中输入netload命令来加载你的自定义插件dll。一般AutoCAD开发过程中你可能需要在你自己的主界面程序里启动AutoCAD并执行你的自定义命令。这时可以通过下面的方式来做。如果你用AutoCAD 2010及以上版本可能会遇到Problem executing component: Call was rejected by callee. (Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))的问题,下文一并分析解决。以下方法同时适用于Map 3D和Civil 3D。
实现外部程序启动AutoCAD
~~~~~~~~~~~~~~~~~~~~~~~~~
在Visual Studio里新建一个Class library的工程,这里命名为myplugin, 编译生成myplugin.dll的程序集。这个项目是你对AutoCAD扩展的主要工作项目,你可以添加AutoCAD相关程序集的引用,并创建自定义命令等等。这个不用多说。如果你需要在自己的主程序窗口中启动AutoCAD。可以在解决方案里创建一个WinForm的项目,比如叫做StartCAD,在Form里放一个button,标题为Start AutoCAD。然后调整你的myplugin的输出路径到StartCAD的bin目录下,方便StartCAD找到你的自定义应用程序集。 如图:
下面实现StartCAD项目中启动AutoCAD并自动加载myplugin.dll . 在StartCAD项目中需要添加如下COM引用:
AutoCAD 2012 Type Library
AutoCAD/ObjectDBX Common 18.0 Type Library
下面是Button1.Click的代码:
private void button1_Click(object sender, EventArgs e)
{
Autodesk.AutoCAD.Interop.AcadApplication cadApp = null;
try
{
//Get the AutoCAD which is running,
cadApp = (Autodesk.AutoCAD.Interop.AcadApplication)Marshal.GetActiveObject(programID);
}
catch
{
try
{
//If AutoCAD is not running, start it
Type sType = Type.GetTypeFromProgID(programID);
cadApp = (Autodesk.AutoCAD.Interop.AcadApplication)Activator.CreateInstance(sType, true);
cadApp.Visible = true;
}
catch (Exception ex)
{
MessageBox.Show("Cannot open AutoCAD. \n Error message : " + ex.Message);
}
}
//send command to AutoCAD to load our custom assembly
if (cadApp != null)
{
cadApp.Visible = true; //[对应AutoCAD 2010 Update1 及以上]
//Load my custom plugin assembly
cadApp.ActiveDocument.SendCommand("filedia\r0\r"); // 关闭文件对话框模式
//通过netload命令加载自定义程序集
cadApp.ActiveDocument.SendCommand("netload\r" + Application.StartupPath + "\\myplugin.dll\r");
//再打开文件对话框模式
cadApp.ActiveDocument.SendCommand("filedia\r1\r");
this.Close();
}
}
可能会遇到的问题及分析
~~~~~~~~~~~~~~~~~~~~~~~~~
如果你用AutoCAD 2010以前版本,上面代码应该没什么问题。但如果你用AutoCAD 2010 Update1及以后版本,你可能会遇到如下错误:
Problem executing component: Call was rejected by callee. (Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))
根据Kean的博客,这个“问题”是由于AutoCAD为了解决接受COM消息时可能出现崩溃的问题而引入的。由于微软决定在WPF中不支持嵌套消息循环,如果WPF正在进行布局处理操作时(这时会调用Dispatcher.DisableProcessing 停止处理消息)又接到COM调用,可能会造成AutoCAD崩溃。所以现在的改进是拒绝这个COM调用,以便让他过一会儿再重新调用。所以就有了上面的错误消息RPC_E_CALL_REJECTED。
解决的方法就是让我们的Form1类实现COM的IMessageFilter接口,这个接口是一个IUnknown接口,他的作用是使得COM服务器或者应用程序能够在等待同步调用响应时选择处理输入或者输入的COM消息。通过这个消息过滤机制,可以让COM 服务器来判定某个调用是否安全,不过造成死锁。COM会调用你的IMessageFilter的实现,从而使得你有机会来对消息做进一步的处理。
IMessageFilter 接口有下面3个方法:
HandleInComingCall 提供了一个输入调用的单一入口
Provides a single entry point for incoming calls.
他的返回值为:
SERVERCALL_ISHANDLED 应用程序也许能够处理这个调用The application might be able to process the call.
SERVERCALL_REJECTED 应用程序由于一些不可预计的问题处理不了这个调用。The application cannot handle the call due to an unforeseen problem, such as network unavailability, or if it is in the process of terminating.
SERVERCALL_RETRYLATER 应用程序现在处理不了The application cannot handle the call at this time. An application might return this value when it is in a user-controlled modal state.
MessagePending COM的等待远程调用响应的时候来了一个消息
Indicates that a message has arrived while COM is waiting to respond to a remote call.
他的返回值为:
PENDINGMSG_CANCELCALL 取消调用,只在极端情况下使用。
Cancel the outgoing call. This should be returned only under extreme conditions. Canceling a call that has not replied or been rejected can create orphan transactions and lose resources. COM fails the original call and returns RPC_E_CALL_CANCELLED.
PENDINGMSG_WAITNOPROCESS 不派发消息继续等待回应
Continue waiting for the reply, and do not dispatch the message unless it is a task-switching or window-activation message. A subsequent message will trigger another call to MessagePending. Leaving messages or events in the queue enables them to be processed normally, if the outgoing call is completed. Note that returning PENDINGMSG_WAITNOPROCESS can cause the message queue to fill.
PENDINGMSG_WAITDEFPROCESS 不再派发键盘和鼠标事件,但派发WM_PAINT消息,任务切换和计划消息正常处理
Keyboard and mouse messages are no longer dispatched. However there are some cases where mouse and keyboard messages could cause the system to deadlock, and in these cases, mouse and keyboard messages are discarded. WM_PAINT messages are dispatched. Task-switching and activation messages are handled as before.
RetryRejectedCall 提供给应用程序一个显示一个以对话框来选择重试,取消还是切换任务的选择。
Provides applications with an opportunity to display a dialog box offering retry, cancel, or task-switching options.
他的返回值是:
-1 调用会取消The call should be canceled. COM then returns RPC_E_CALL_REJECTED from the original method call. 0 ≤ value < 100 调用会立即重试The call is to be retried immediately.
100 ≤ value 调用会在指定时间后重试,以毫秒计。COM will wait for this many milliseconds and then retry the call.
解决办法
~~~~~~~~~~~~~~~~~~~~~~~~~
上面提到AutoCAD在WPF进行布局处理时拒绝了COM调用的消息,我们可以实现一个IMessageFilter的接口,等待一段时间再重新调用,下面是改进后的代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Autodesk.AutoCAD.Interop;
using Autodesk.AutoCAD.Interop.Common;
namespace StartAutoCAD
{
// about IMessageFilter Interface http://msdn.microsoft.com/zh-cn/library/ms693740%28v=VS.85%29.aspx
[ComImport,
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("00000016-0000-0000-C000-000000000046")]
public interface IMessageFilter
{
[PreserveSig]
int HandleInComingCall(int dwCallType,
IntPtr hTaskCaller,
int dwTickCount,
IntPtr lpInterfaceInfo);
[PreserveSig]
int MessagePending(IntPtr hTaskCallee,
int dwTickCount,
int dwPendingType);
[PreserveSig]
int RetryRejectedCall(IntPtr hTaskCallee,
int dwTickCount,
int dwRejectType);
}
public partial class Form1 : Form, IMessageFilter
{
string programID = "AutoCAD.Application";
[DllImport("ole32.dll")]
static extern int CoRegisterMessageFilter(
IMessageFilter lpMessageFilter,
out IMessageFilter lplpMessageFilter
);
public Form1()
{
InitializeComponent();
IMessageFilter oldFilter;
CoRegisterMessageFilter(this, out oldFilter);
}
private void button1_Click(object sender, EventArgs e)
{
Autodesk.AutoCAD.Interop.AcadApplication cadApp = null;
try
{
//Get the AutoCAD which is running
cadApp = (Autodesk.AutoCAD.Interop.AcadApplication)Marshal.GetActiveObject(programID);
}
catch
{
try
{
Type sType = Type.GetTypeFromProgID(programID);
cadApp = (Autodesk.AutoCAD.Interop.AcadApplication)Activator.CreateInstance(sType, true);
cadApp.Visible = true;
}
catch (Exception ex)
{
MessageBox.Show("Cannot open AutoCAD. \n Error message : " + ex.Message);
}
}
if (cadApp != null)
{
cadApp.Visible = true;
//Load my custom plugin assembly
cadApp.ActiveDocument.SendCommand("filedia\r0\r");
cadApp.ActiveDocument.SendCommand("netload\r" + Application.StartupPath + "\\myplugin.dll\r");
cadApp.ActiveDocument.SendCommand("filedia\r1\r");
this.Close();
}
}
#region IMessageFilter Members
int IMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
{
return 0; // SERVERCALL_ISHANDLED
}
int IMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
{
return 1; // PENDINGMSG_WAITNOPROCESS
}
int IMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
{
return 1000; // Retry in a second
}
#endregion
}
}