# 用 aardio 创建 ActiveX 控件

在 aardio 中可以创建 ActiveX EXE ，用于实现进程外 COM 控件。
可在其他支持 COM 控件的编程环境中嵌入入或调用 aardio 编写的 ActiveX 控件。

## 创建 ActiveX 工程

在 aardio 开发环境中点击『主菜单 » 新建工程』打开工程向导。
在工程向导中选择创建『窗口程序 » ActiveX EXE 』工程，并在向导界面输入 COM 控件类名 （也就是 ProgID ）。例如我们输入 COM 控件类名 `aardioTestControl.Sample` 就会自动在工程中创建 `/lib/aardioTestControl/Sample.aardio` 这个类库。

创建 ActiveX  工程以后，我们只要简单地打开 `/lib/aardioTestControl/Sample.aardio`  添加我们需要导出的 COM 接口函数即可。

一般使用  ActiveX 工程向导自动生成的 COM 类型库（ typelib ) 就可以了，不需要学习复杂的 ODL 语法。

将   ActiveX 工程发布为 EXE 文件以后，直接双击运行   ActiveX EXE 就可以注册、卸载 COM 控件。

## ActiveX 工程源码解析  - main.aardio

创建 ActiveX 工程以后，我们在打开负责启动程序的主代码文件 `/main.aardio`，可以看到关键代码如下：

```aardio
import com.activeX;

//导入 aardio 类库
import aardioTestControl.Sample;

//将 aardio 类库导出为 COM 接口，参数指定的 aardio 类名名称也就是 ProgID。
var activeX = com.activeX("aardioTestControl.Sample") 

//启动 ActiveX 控件。
var ok,message = activeX.main();
```

上面的代码依次执行了以下步骤的操作：

1. 我们通过 `import aardioTestControl.Sample` 导入了 aardio 类库。

2. 然后我们用 com.activeX("aardioTestControl.Sample")  将 aardioTestControl.Sample 这个类库的名称指定为 ProgID .

3. 最后我们使用 `activeX.main()` 启动了 ActiveX 控件。

`activeX.main()` 的返回值又分为以下几种情况：

- 如果是外部程序通过 COM 接口调用 aardioTestControl.Sample 成功则返回值 ok 为 true 而且返回值 message 为 null。
- 如果返回值 ok 为 false 而且返回值 message 为 null，则说明是用户自行运行 EXE（例如在资源管理器又击启动 ActiveX EXE 程序），这时候可以显示 ActiveX 注册向导。
- 如果返回值 ok 为 false 而且返回值 message 不为 null，则 message 是注册或卸载 ActiveX 控件的回显信息（字符串类型）。

检查 `activeX.main()` 返回值的示例代码如下：

```aardio
var ok,message = activeX.main();

if( !ok && !message  ) {  
	var id = win.msgbox("可用的启动参数如下:
	
/r 或 /RegServerPerUser 注册组件
/u 或 /UnregServerPerUser 卸载组件

按『是』注册控件，按『否』卸载控件","activeX控件演示",3/*_MB_YESNOCANCEL*/)   
	
    if( id == 6/*_IDYES*/ ){
		activeX.register(); 
		win.msgbox("注册组件成功");
    }
    elseif( id ==7/*_IDNO*/ ) {
    	activeX.unRegister();
    	win.msgbox("卸载组件成功");
    } 
} 
elseif( message ){
	//通过命令行参数注册或卸载控件回显信息
	if(!ok) win.msgbox(message); 
}
else {
	//ok为真,message为空表示组件成功启动
}

```

## ActiveX 工程源码解析  - COM 类库

我们看一下 COM 类库 `/lib/aardioTestControl/Sample.aardio` 的主要源码：

```aardio
namespace aardioTestControl;

class Sample{
	
	//构造函数，参数 activeX 为 com.activeX 实例对象。
	ctor(activeX){
		this.activeX = activeX;
	}
	

	//自定义需要导出的 COM 按口函数
	Add = function(a,b){
		return a + b
	} 	 
	
	//以下为 IDispatchExecutable 接口函数
		
	Quit = function(){ 
		
		// COM控件调用方必须调用这个函数退出进程
		this.activeX.stop(); 
	}
	
	Invoke = function(methodName,...){
		
		//模仿 owner call 方式获取函数对象，owner call 指的是以 ownerObject.methodName() 格式调用函数，以保证函数内的 owner 参数指向 ownerObject 对象。
		var f = ..table.getCall(this,methodName);
		return invoke(f,this,...);
	}
	
	GetAttr = function(k){
		return ..table.getAttr(this,k);
	}
	
	SetAttr = function(k,v){
		return ..table.setAttr(this,k,v);
	}

	GetItem = function(k){
		return this.Item[k];
	}
	
	SetItem = function(k,v){
		this.Item[k] = v;
	}
	
	Item = { }
	
	//指定对象的元表
	@{
		/*
		_item 元方法可指向返回表的函数，也可以直接返回一个表。 
		_item 返回的表用于指定查询 COM 默认属性（DISPID_VALUE）的表对象，同时也是 COM 枚举表。
		*/
		_item = lambda() owner.Item; 
		
	}
} 

```

aardio 中创建的 ActiveX 类库默认继承自 COM 接口 IDispatchExecutable，所以必须实现 IDispatchExecutable 规定的接口函数。

aardio 表对象、函数对象、cdata 对象在 COM 接口中都会自动转换为 IDispatch 对象。导入 COM 控件的外部程序可以直接调用 IDispatch 对象。 

请参考: [COM 类型](../datatype.md)

有两个小细节需要注意：

- AutoLISP 使用 vlax-invoke 调用 COM 控件，符号名默认会转为大写，而 aardio 是大小写敏感的编程语言。所以在这种情况下，我们要明确实大写函数名的导出函数。
- 如果要在 COM 接口函数中实现输出参数，则必须编辑 ODL 以自定义 COM 类型库。请参考 [ODL 语法指南](odl.md)

下面是一个在导出的 COM 接口函数中使用输出参数的示例：

```aardio
namespace aardioTestControl;

class Sample{
	
	ctor(activeX){
		this.activeX = activeX;
	}

	TestOutParams = function(str,out1,out2){
		//输出参数必须也是输入参数。
		
		out1 = 1;
		out2 = 2;
		
		//输出参数必须增加到返回值列表。
		return out1,out2;
	}

	//省略其他代码
}
```

对应的 ODL 接口声明：


```odl
	[ uuid(1C8736BC-8C0C-4DB6-9FAD-1C6A0CDF1FA2) ]
	dispinterface  IDispatchSample{ 
		properties:
		methods:  
			[ id(10) ]
			void TestOutParams( [in] BSTR str,[in,out] VARIANT *out1,[in,out] VARIANT *out2);
	} 
```

> 要注意在 ODL 声明中所有自定义的 DISPID 不应大于 10000 （这是因为 aardio 自动生成的 DISPID 以 10000 为起始值递增）。

下面重点说一下 COM 默认属性与 COM 枚举器：

COM 默认调用的 DISPID 为: DISPID_VALUE 。
例如在 VB 或 VBA 中用 `comObj("项目名") = 值` 写默认属性，用 `v = comObj("项目名") ` 读默认属性。 

- 普通 aardio 函数则可支持匿名的 COM 默认调用。函数对象的 COM 默认调用指向自身，不可修改。
- 对 aardio 表对象的 COM 默认调用会读写表对象的自身属性，COM 枚举器则默认会遍历表对象自身的成员。
- 表对象与 cdata 对象可使用 _call 元方法响应 COM 默认调用，这时候 _call 元方法必须指向一个函数对象。
- 表对象也可使用 _item 元方法获取一个指定 COM 默认属性集合的 aardio 表对象，通过 _item 元方法获取的表同时也会被当作 COM 枚举表（ 也就是说 COM 枚举器会枚举 _item 返回的表 ）。_item 元方法可以指向一个返回表的函数对象，也可以直接指定一个表对象。


## 在 ActiveX 类库中创建窗口

如果要在 ActiveX 控件中创建窗口，则导出的 COM 类库至少要实现 CreateWindow,DestroyWindow,SetExtent 这三个接口函数。

```aardio
import aardioTestControl.SampleForm;
namespace aardioTestControl;

class Sample{
	
	ctor(activeX){
		this.activeX = activeX;
	}
	
	/*
	创建窗口。
	hwndParent 参数为父窗口句柄。
	x,y 为控件坐标。
	cx,cy 为控件宽度与高度。
	*/	
	CreateWindow = function(hwndParent, x, y, cx, cy){
		this.form = ..aardioTestControl.SampleForm(hwndParent);
		this.form.setPos(x, y, cx, cy);
		this.form.onDestroy = function(){
			this.activeX.stop();
		}
		
		this.form.external = this;
		this.form.externalEvent = this.externalEvent;
		return this.form.hwnd;
	}
	
	//销毁窗口事件
	DestroyWindow = function(){
		this.form.close();
		..win.quitMessage();
	} 
	
	//自适应窗口大小
	SetExtent = function(cx,cy){
		this.form.resize(cx,cy)	
		return true;
	} 
	
	//省略其他代码
} 
```

创建窗口的 aardioTestControl.SampleForm 类库源码如下：

```aardio
import win.ui;

namespace aardioTestControl;
class SampleForm{
	ctor(parent,tParam){
	/*DSG{{*/
	this = ..win.form(text="test.control";right=757;bottom=467;border="none";exmode="none";mode="child";parent=parent;tParam=tParam)
	this.add(
	button={cls="button";text="Button";left=71;top=60;right=283;bottom=150;z=1}
	)
	/*}}*/
	
	this.button.oncommand = function(id,event){
		/*
		所有用 com.activeX 的 COM 类库创建的对象都会增加事件对象 externalEvent 。
		通过 externalEvent 可以触发调用方指定的 COM 事件。
		
		注意 VB6 不支持后期绑定事件，需要在 odl 事件接口中明声明事件函数。
		*/
		this.externalEvent.onHello("hello")
	}

	this.show()
    };
}

if( !owner ) ..aardioTestControl.SampleForm().doModal();

/**intellisense()
aardioTestControl.SampleForm() = 窗口类\n!winform.
end intellisense**/
```

## ODL 类型声明

aardio 工程向导创建的 ActiveX 工程已提供了默认的 `/typelib/typelib.odl`，一般不需要修改。

如果要改动 ODL，请参考 请参考 [ODL 语法指南](odl.md)

默认的 `/typelib/typelib.odl` 内容如下：

```aardio
import "aardio.idl";

[
uuid(D0A0632B-C202-48FA-9C7B-32E808D233EB),
version(1.0)
]
library aardioTestControl {

	importlib("stdole32.tlb"); 
	
	
	[ uuid(A880101D-7B64-4F82-B568-FDE288A488A8) ]
	dispinterface  IDispatchExecutableWithEvent  { 
	
		properties:
		methods: 
			/*
			VB 不支持后期绑定事件接口，
			可将下面的 IDispatchExecutableEvent 换为 IDispatchExecutableWithEvent 。
			并在下面明确添加接口函数。
			
			注意下面的 id（DISPID） 不要小于等于 0 也不要大于 10000。
			*/
			[ id(10) ]
			void onHello( [in] BSTR str );
	
	};
	
	[ uuid(822687D3-D4C8-4B02-8959-03957C3DE586) ]
	dispinterface IDispatchSampleSample { 
		properties:
		methods:  
			/*
			输出参数必须是声明为 [in,out] VARIANT * paramName，
			并在实现接口的 aardio 函数的返回值列表中增加对应的输出参数。 
			*/
			[ id(10) ]
			void TestOutParams( [in] BSTR str,[in,out] VARIANT *out1,[in,out] VARIANT *out2);

			//dispinterface 接口不能继承，有需要可以到 /.build/aardio.idl 中复制 IDispatchExecutable 的定义。
	
	};
	
	[ uuid(257D8BF9-426A-424A-8FA8-7F9701E784A2),control ]
	coclass Sample {
		
		//默认接口
		[default] dispinterface IDispatchExecutable;
		
		//默认事件源接口
		[default,source] dispinterface IDispatchExecutableEvent;
	}; 
};
```



