在 aardio 中可以调用很多编程语言,例如 C语言、C++、C#、Java、Python、R、Javascript、Node.Js、Fortran、VB、Flash ActionScript、PHP、VBScript、PowerShell、NewLISP、AutoLISP、Delphi、FreeBASIC、Ruby、Rust、Julia、Nim、Go 语言、批处理 …
本文我们主要介绍 aardio + AutoHotkey 混合编程。
使用 ahk 扩展库
1、双击打开:aardio 工具 / 扩展库
2、扩展库名字输入 ahk,搜索结果中双击 ahk 扩展库 —— 打开最新范例。
aardio 提供了 ahk 扩展库可用于嵌入 AutoHotkey 并与 AutoHotkey 交互调用,支持 AutoHotkey v1 以及 AutoHotkey v2 版本。
aardio 调用 AutoHotkey v1
aardio 可以直接调用 AutoHotkey 函数,AutoHotkey 也可以调用 aardio 函数,非常简单:
import win.ui;
/*DSG{{*/
var winform = win.form(text="嵌入 AutoHotkey v1 演示")
winform.add(
edit={cls="edit";left=17;top=18;right=741;bottom=445;edge=1;multiline=1;z=1}
)
/*}}*/
//导入 AutoHotkey v1 扩展库
import ahk;
/*
导出允许AutoHotkey 调用的 aardio 函数(函数名必须以 on 开头)。
*/
ahk.onTest = function(param){
winform.edit.print( "winform.onTest被ahk调用",param );
winform.show();
}
//运行ahk代码
ahk.run( `
#Persistent ;保持 AHK 线程不退出
#NoTrayIcon ;不显示托盘图标
#include <aardio> ;启动 AHK 代码中必须加入这句才能与 aardio 交互
; 定义函数与 aardio 有点像,aardio 一般会写为 GetPoint = function(hwnd){ return value;}
GetPoint(){
; 直接调用aardio函数,支持任意类型、不定个数的调用参数
aardio.onTest({ x : 2, y : 3 })
return { x : 2, y : 3 }
}`);
/*
直接调用ahk里定义的全局函数,
参数和返回值自动支持各种数据类型,就像调用普通aardio函数。
是不是很神奇?!
*/
var point = ahk.GetPoint();
winform.edit.print("调用ahk函数的返回值:",point);
win.loopMessage();
导出 aardio 函数给 ahk 时也可以返回一个异步执行的函数对象,调用虚拟桌面示例:
import ahk;
import dotNet.desktop;
import win.ui;
/*DSG{{*/
var winform = win.form(text="虚拟桌面切换";right=599;bottom=399)
winform.add(
edit={cls="edit";left=17;top=18;right=565;bottom=374;edge=1;multiline=1;z=1}
)
/*}}*/
//导出 AutoHotkey 函数
ahk.onSwitch = function(index){
winform.edit.print("已切换桌面到:",index);
//虚拟桌面在键盘钩子中需要通过返回异步函数异步调用。
return function() {
dotNet.desktop.get(index).MakeVisible()
}
}
//运行 AutoHotkey 代码注册热键
ahk.run( `
#Persistent
#NoTrayIcon
#include <aardio>
^j:: aardio.onSwitch(1)
^k:: aardio.onSwitch(2)
`);
winform.show();
win.loopMessage();
请注意 aardio 中文件路径以斜杆(或反斜杆)开始表示应用程序根目录,应用程序根目录在开发时指工程目录或工程外独立启动的文件所在目录,发布后指 EXE 所在目录。应用程序根目录已设为 AHK 工作目录 A_WorkingDir 。
如果在 aardio 中执行以下代码:
ahk.import("path/libName");
则由 aardio 导入文件:
\AutoHotkey\Lib\path\libName.ahk
可以在 aardio 工程中添加上面的 ahk 文件 到内嵌资源目录,这样该 ahk 文件会被编译到 exe 资源内,运行时自动转存为以下文件:
%A_MyDocuments%\AutoHotkey\Lib\path\libName.ahk
在 AHK 中可以使用以下代码引用该模块:
#include <path/libName>
在 aardio 中可使用 ahk.setGlobal() 函数修改 AHK 全局变量:
//直接修改 AHK 全局变量的值
ahk.setGlobal("gVar","测试一下")
在 aardio 中执行 AHK 代码时还可以支持 aardio 模板语法,也就是在 AHK 代码中可以直接写 aardio 代码,只要将 aardio 代码包含在 <? ?> 标记中就可以了(类似 PHP 模板语法),例如:
//运行 AHK 代码
ahk.run( `
#Persistent ;
#NoTrayIcon ;
#include <aardio> ;
global gVar := "<?="允许嵌入 aardio 代码"?>" ;
`);
//直接修改 AHK 全局变量的值
ahk.setGlobal("gVar","测试一下")
关于 aardio 模板语法请参考 aardio 文档。
aardio 调用 AutoHotkey v2
在 aardio 使用 ahk.thread 扩展库可以调用 AutoHotkey v2,ahk.thread 不再需要在 AHK 启动代码中包含 #include ,其他用法与调用 ahk 扩展库类似。下面看例子:
import win.ui;
/*DSG{{*/
var winform = win.form(text="嵌入 AutoHotkey v2 演示")
winform.add(
edit={cls="edit";left=8;top=10;right=749;bottom=452;edge=1;multiline=1;z=1}
)
/*}}*/
winform.show();
import ahk.thread;
var ahk2 = ahk.thread();
//直接定义允许 AHK 调用的 aardio 函数(函数名以on开头)
ahk2.onTest = function(param){
winform.edit.print( "winform.onTest被ahk调用",param );
return 123
}
//运行 AHK 代码
ahk2.run( `
#NoTrayIcon ;不显示托盘图标
Persistent( true) ;保持ahk线程不退出
; 定义函数与 aardio 有点像,aardio 一般会写为 GetPoint = function(hwnd){ return value;}
GetPoint(){
; 直接调用aardio函数,支持任意类型、不定个数的调用参数
aardio.onTest({ x : 2, y : 3 })
return { x : 2, y : 3 }
}`);
/*
直接调用ahk里定义的全局函数,
参数和返回值自动支持各种数据类型,就像调用普通aardio函数。
是不是很神奇?!
*/
var point = ahk2.GetPoint();
winform.edit.print("调用ahk函数的返回值:",point);
win.loopMessage();
要注意 AHK 中的变量默认是局部变量,定义全局变量要用 global 语句,aardio 则是默认全局变量,定义局部变量要用 var 语句。
进阶
有了前面的基础,我们再将范例稍微整复杂一点,例如调用 AHK v1 :
import win.ui;
/*DSG{{*/
var winform = win.form(text="嵌入 AutoHotkey v1")
winform.add(
edit={cls="edit";left=17;top=18;right=734;bottom=444;edge=1;multiline=1;z=1}
)
/*}}*/
import ahk;
ahk.onTest = function(param){
winform.edit.print( "winform.onTest被ahk调用",param );
winform.show();
}
ahk.run( `
#Persistent
#NoTrayIcon
#include <aardio>
GetPoint(){
; 命令式语法 = 号后面是字符串,变量要放到百分号里
POINT_TYPE =
(
UInt x;
UInt y;
) ; 首尾两个括号都必须独占一行,用于包含多行字符串
; 新语法 := 后面是表达式,和其他编程语言类似字符串放引号里,变量不用放百分号里
point := Struct( POINT_TYPE ) ;
; 取结构体指针,aardio 里调用 API 时结构体总是自动转为指针,不需要写这些代码
ptr := point[]
; DllCall("user32.dll\GetCursorPos", "Ptr", ptr)
GetCursorPos(ptr) ;
; 直接调用aardio函数,支持任意类型、不定个数的调用参数
aardio.onTest({ x : point.x, y : point.y })
; 命令式语法, 百分号加一个空格表示后面是表达式
MsgBox, % "x:" point.x ",y:" point.y
return { x : point.x, y : point.y }
}`);
/*
直接调用ahk里定义的全局函数
*/
var point = ahk.GetPoint();
winform.edit.print("调用ahk函数的返回值:",point);
win.loopMessage();
以上主要是演示 aardio 与 AHK 交互,其实 aardio 直接调用 ::User32.GetCursorPos() 代码更简单,示例:
// 当然上面的代码用 aardio 直接写会更简单,例如:
var point = {int x;int y};
//不用声明直接调用 API,不用写参数类型这些
::User32.GetCursorPos(point);
aardio 可以免声明直接调用 WinAPI ,不需要指定参数类型。
下面再来个例子,使用 aardio 暂停、继续 AHK 脚本:
import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text="嵌入autohotkey演示";right=757;bottom=467)
winform.add(
button={cls="plus";text="暂停AHK";left=19;top=11;right=109;bottom=41;bgcolor=-6960024;font=LOGFONT(name='FontAwesome';charset=0);z=2};
edit={cls="edit";left=17;top=56;right=729;bottom=443;edge=1;multiline=1;z=1}
)
/*}}*/
import ahk;
var handle = ahk.run( `
#Persistent ;保持ahk线程不退出
#NoTrayIcon ;不显示托盘图标
#include <aardio> ;启动ahk代码中必须加入这句才能与aardio交互
::hj::run无乱码
`);
winform.button.onMouseClick = function(){
if(winform.button.text=="暂停AHK"){
thread.suspend(handle)
winform.button.text = "继续AHK"
}
else {
thread.resume(handle)
winform.button.text = "暂停AHK"
}
}
winform.button.skin({
color={
active=0xFFFFFFFF;
default=0xFF000000;
hover=0xFFFFFFFF
};
});
winform.show();
win.loopMessage();
附:《 aardio 语法速览 》
第一个控制台程序
//导入库
import console;
//加载动画
console.showLoading("加载动画测试");
sleep(1000); //暂停 1 秒
//输出字符串
console.log('测试字符串');
//输出变量
console.dump({a=123;b=456});
//以 JSON 格式输出变量
console.dumpJson({a=123;b=456});
//暂停并等待按键
console.pause(true);
第一个窗口程序
import win.ui;
/*DSG{{*/
var winform = win.form(text="aardio form")
winform.add(
button={cls="button";text="测试";left=435;top=395;right=680;bottom=450;color=14120960;font=LOGFONT(h=-14);note="点这里";z=2};
edit={cls="edit";left=18;top=16;right=741;bottom=361;edge=1;multiline=1;z=1}
)
/*}}*/
//按钮回调事件
winform.button.oncommand = function(){
winform.edit.text = "测试"; //修改控件属性
}
//显示窗口
winform.show();
//启动界面消息循环,保持窗口显示,并响应用户操作
win.loopMessage();
调用静态 API
//加载 DLL
var dll = raw.loadDll("user32.dll");
//调用 API 函数
dll.MessageBox(0,"测试","标题",0);
aardio 已经默认加载了一些常用的系统 DLL 对象,例如 ::User32, ::Kernel32, ::Shell32,::Ntdll 等。
所以上面的代码可以简化为:
::User32.MessageBox(0,"测试","标题",0);
静态结构体
var info = {
INT size = 8;
INT time;
}
::User32.GetLastInputInfo( info )
因为 aardio 函数支持多返回值,且结构体默认为传址输出参数 —— 会添加到返回值中,所以上面的代码也可以这样写:
var ok,info = ::User32.GetLastInputInfo( {
INT size = 8;
INT time;
} );
注释语句
// 单行注释
/*
多行注释,首尾星号数目必须相同。
*/
变量、赋值
var n = null;
var str = "字符串";
aardio 中的变量默认为当前名字空间变量,使用 var 语句可以定义局部变量。
语句、语句块
{
var n = null;
var str = "字符串";
}
语句尾部可以加分号也可以不加。
语句块首尾加 { } 标记,var 语句支持语句块作用域。
数值
var num = 123.01; //10 进制数值
var hex = 0xEFEF; //16 进制数值
var num2 = 123_456; //可用下划线作为分隔符
var num3 = 2#010; //可在 # 号前自定义进制
算术运算
var num = 3 + 2; //加,值为 5
var num = 3 - 2; //减,值为 1
var num = 3 * 2; //乘,值为 6
var num = 3 / 2; //除,值为 1.5
var num = 3 % 2; //取模,值为 1
var num = 3 ** 2; //乘方,值为 9
位运算
import console;
//按位取反
var n = ~2#101;
//显示2#11111111111111111111111111111010
console.printf("2#%b", n)
//按位与
var n = 2#101 & 2#100;
//显示2#100
console.printf("2#%b", n)
//按位或
var n = 2#101 | 2#110;
//显示2#111
console.printf("2#%b", n)
//按位异或
var n = 2#101 ^ 2#110;
//显示2#011
console.printf("2#%b", n);
//按位左移
var n = 2#101 << 1;
//显示2#1010
console.printf("2#%b", n);
//按位右移
var n = 2#101 >> 1;
//显示2#10
console.printf("2#%b", n);
//按位无符号右移
var n = -2 >>> 18;
//显示 16383
console.log(n);
console.pause();
字符串
var str = "普通字符串。
可直接包含换行符,不能包含回车符。
可以用 "" 表示一个双引号。";
var str2 = `反引号的作用与双引号一样。
可直接包含换行符,不能包含回车符。
可以用 `` 表示一个反引号。`;
var str3 = '转义字符串
忽略回车换行,反斜杆作为转义符使用
例如 \r\n 表示回车换行';
var str4 = /*
段注释可以赋值为字符串。
首尾星号数目必须配对。
忽略首尾紧邻星号的第一个空行(如果有的话),
其他换行总是会规范化为回车换行符,
也就是 '\r\n'。
*/
var str5 = //行注释也可赋值为字符串,不含回车换行
//在文件路径前加 $ 符号,可将该文件编译为字符串
var str6 = $"~/aardio.exe"
二进制字符串、文本编码
var bin = '字符串可以包含任意二进制数据,例如 \0'
var utf8 = "字符串可以包含文本,默认为 UTF-8 编码"
var utf16 = '转义字符串后加 u 字符表示 UTF-16 编码的 字符串'u
aardio 在很多地方都支持自动编码转换,例如调用 Unicode(UTF-16) 版本的 API 函数时,UTF-8 字符串可自动转换为UTF-16 编码字符串( 支持双向自动转换),例如以下两句代码的作用是相同的:
::User32.MessageBox(0,"内容","标题",0);
::User32.MessageBox(0,'内容'u,'标题'u,0);
在调用 ::User32.MessageBox 时,aardio 会自动检测并优先使用 Unicode 版本的 ::User32.MessageBoxW() 函数。当然,也可以主动在 API 函数名后加上大写的 W 尾标声明这是 Unicode 版本函数(即使该函数名尾部并没有 W,也可以添加 W 尾标)。
字符串连接
//使用 ++ 操作符连接字符串
var str = "字符串1" ++ "字符串2";
//如果 ++ 前后有引号,可省略一个 + 号。
var str = "字符串1" + "字符串2";
//用 string.concat() 函数连接支持多参数,支持 null 值
var str = string.concat("字符串1","字符串2")
读写文件
//写文件
string.save("/test.txt","要保存在文件的字符串")
//读文件
var str = string.load("/test.txt");
文件路径
/*
文件路径开始可用单个斜杠(或反斜杠)表示应用程序根目录。
应用程序根目录在开发时指工程目录,发布后指 exe 目录。
*/
var str = string.load("\res\test.txt");
//文件路径中正斜杠可自动转换为反斜杠
var str = string.load("/res/test.txt");
/*
文件路径开始用波浪线表示 exe 所在目录。
aardio 不需要生成 exe 就可以运行调试,此时波浪线表示 aardio.exe 所在目录。
*/
var str = string.load("~/res/test.txt");
/*
aardio 很多读文件的函数都兼容工程内嵌资源目录。
不需要修改代码,所以 "\res\test.txt" 可以是资源目录。
如果读取文件失败,string.load() 会返回 null 值(不报错)。
*/
var str = string.load("\res\test.txt");
/*
将文件转换为完整路径。
将路径传给第三方组件时,建议这样转换一下。
*/
var path = io.fullpath("\res\test.txt");
注意在 aardio 中双引号内不需要转义反斜杠。所以不用写为 “\res\test.txt”,单引号内才需要这样写。
在 aardio 中一般不必要使用 “./dir/test.txt” 这样的相对当前目录路径。因为当前目录可以任意更改,你不知道什么时候某个第三方组件是不是悄悄帮你改了当前目录。
嵌入文件到字符串
在文件路径前加上 $ 操作符可将该文件编译为字符串对象。
在编译该代码时文件必须存在,程序发布后就不再需要这个文件了。
示例:
var str = $"\dir\test.txt";
不要用这个方法包含资源目录下的资源文件,
因为这等于将同一个文件重复包含了多次,会不必要地增加发布体积。
名字空间
//导入其他库文件,同时也导入该库创建的名字空间
import console;
//打开名字空间
namespace test.a.b{
//定义当前名字空间变量
console = 123;
//..console 等价于 global.console
..console.log(console);
}
console.pause(true);
默认名字空间为全局名字空间,也就是 global 名字空间。在其他名字空间访问 global 名字空间必须在变量前加上两点( … ) 。例如 global.console 等价于 …console 。
在 aardio 中使用 import 导入其他库文件,同时也导入该库创建的名字空间。
导入名字空间只是引入模块,访问该名字空间仍然要写完整的名字空间路径。
表 (table)
table(表)是aardio中唯一的复合数据类型,除了非复合的基础数据类型以外,aardio 中几乎所有的复合对象都是表,即使是变量的命名空间也是表。表的本质是一个集合,可以用于容纳其他的数据成员,并且表也可以嵌套的包含其他的表(table),在aardio里表几乎可以容纳一切其他对象。
表可以包含键值对:
tab = {
a = 123;
str = "字符串";
[123] = "不符合变量命名规则的键应放在下标内。";
["键 名"] = "不符合变量命名规则的键应放在下标内。";
键名 = {
test = "表也可以包含表";
}
}
表也可以包含数组:
//在表中创建数组
var array = {
[1] = 123;
[2] = "数组的值可以是任何其他对象";
[3] = { "也可以嵌套包含其他表或数组"}
}
//数组的键可以省略,下面这样写也可以(并且建议省略)
var array = {
123;
"数组的值可以是任何其他对象";
{ "也可以嵌套包含其他表或数组"}
}
在表中允许使用类 JavaScript 语法,用冒号代替等号分隔键值,用逗号代替分号分隔元素,并可用引号包含键名。
示例:
var tag ={"name1":123,"name2":456}
注意如果键名以下划线开始,则为常量字段(值为 非 null 值时不可修改) —— 可通过元属性 _readonly 禁用此规则(参考语法手册)。
成员操作符
var tab = {a = 123; b = 456};
/*
用点号访问表成员,点号后面必须跟合法的标识符
*/
var item = tab.a;
/*
用下标操符符 [] 访问表成员,[] 里可以放任何表达式。
*/
var item = tab["a"];
/*
用直接下标操符符 [[]] 访问表成员,[[]] 里可以放任何表达式。
直接下标不会触发元方法,用于 null 值也会返回 null 而不是报错。
*/
var item = tab[["a"]];
null 值
未定义的变量值默认为 null 。
获取表对象不存在的成员值时也会返回 null 而不会报错。
如果对象本身是 null 值,使用点号、普通下标获取成员值会报错,改用直接下标 [] 则不会报错,而是会返回 null 值。
用 # 操作符获取 null 值长度返回 0 而不是报错。
将表对象的成员赋值为 null 就可以删除该成员。
在逻辑运算中,null 值为 false。
取数组或字符串长度
import console;
var array = {123;456};
console.log("数组长度",#array);
var str = "abcd";
console.log("字符串长度",#str);
var n = null;
console.log("null 值长度为 0",#n);
console.pause();
使用 # 操作取数组或字符串的值,也可以取 null 值的长度( 返回 0 )。
if 语句
import console;
var enabled = true;
if (enabled) {
console.log('enabled 的值为 true');
}
console.pause(true);
注意在逻辑运算中,非 0、非 null、非 false 为 true,反之为 false。
如果要准确判断一个变量的值是否为 true 或 false ,则应使用恒等操作符,如下:
import console;
var enabled = false;
if (enabled === false ) {
console.log('enabled 的值为 false,不是 0,也不是 null');
}
elseif( enabled ){
console.log('enabled 的值为真');
}
else{
console.log('enabled 值为:',enabled);
}
console.pause(true);
上面的 elseif 也可以改为 else if。
逻辑操作符
逻辑非 既可以用 not 也可以用 ! 。
逻辑与 既可以用 and 也可以用 && 或者 ? 。
逻辑与 既可以用 or 也可以用 || 或者 : 。
aardio 语句在设计时试图兼容其他语言的一些基本语法,所以很多语法有类似上面的替代写法,例如可以用 begin end 替代 { } 包含语句块。
三元运算符
var n = 1;
// ret 值为 true
var ret = n ? true : 0;
要特别注意 ? 实际上是逻辑与操作符,: 实际上是逻辑或操作符。如果 a ? b : c 这个表达式里 b 为 false,则该表达式总是返回 c。这与其他类 C 语言的三元操作符不同,请注意区分。
定义函数
//定义函数:
add = function(a, b) {
return a + b,"支持多个返回值";
}
//调用函数
var num,str = add(1, 2);
/*
要特别注意,函数可以返回多个值。
可以用 () 将多个返回值转换为单个调用参数。
*/
var str = tostring( ( add(1, 2) ) );
for 循环语句
import console;
for(i=1;10;1){
console.log(i);
}
console.pause();
for in 循环语句
import console;
var tab = {
a = 123;
b = 456;
}
for(k,v in tab){
console.log(k,v)
}
console.pause();
while 循环语句
import console;
var i = 0;
while (i < 100) {
i++;
console.log(i);
if(i==10){
break; //中止循环
}
}
console.pause();
while var 语句
import console;
//用法1,while条件中使用一个var语句
while(
var i = console.getNumber( "请输入数值,输入0退出:" )
) {
console.log( i )
}
//用法2,while条件中使用var语句、循环前执行语句、条件判断语句
while(
var i;
i = console.getNumber( "请输入数值,输入100退出:" );
i != 100 ) {
console.log( i )
}
//用法3,省略var语句,仅使用循环前执行语句、条件判断语句
var i = 0;
while( ;i++; i<10 ) {
console.log(i)
}
//用法4,省略var语句,循环前执行语句,仅使用条件判断语句
while(i>0){
i--;
console.log(i);
}
console.pause(true);
while var 模拟 for 循环
import console;
while( var i = 0; i++ ; i < 5 ) {
console.log( i )
}
console.pause(true);
break label, continue label, break N, continue N 语句
break 语句中断并退出当前循环语句。
continue 语句跳过当前循环体剩下的部分,继续执行下一次循环。
break label,continue label 通过为循环指定一个标号以中断指定的循环。
break N,continue N 通过指定循环语句的嵌套层次以中断指定的循环。
import console;
var i, j;
for (i = 0; 2; 1 ) { loop1:
for (j = 0; 2; 1) { loop2:
if (i == 1 && j == 1) {
continue loop1;
}
else {
console.log("i = " + i + ", j = " + j);
}
}
}
console.pause(true);
注意循环标号写在循环体内部开始处。
类
//定义类
class cls{
//构造函数
ctor(name,...){
this.name = name;
this.args = {...}
};
//属性
value = staticNum;
//成员函数
add = function(v){
this.value = this.value + v;
return this.value;
}
}
//打开类的名字空间
namespace cls{
staticVar = "类的静态变量值";
staticNum = 2;
}
//调用类创建对象
var obj = cls();
//调用对象函数
var v = obj.add(5);
类内部可以用 this 访问当前创建的实例对象。
类总是先调用构造函数 ctor 才会初始化其他成员。
每个类都有自己的名字空间。
类创建的所有实例共享同一名字空间。
类名字空间的成员也就是类的静态成员。
self,this,owner
self 表示当前名字空间。
this 在类内部表示当前创建的实例对象。
每个函数都有自己的 owner 参数( 无法使用外部函数的 owner 参数 )。如果用点号作为成员操作符获取并调用对象的成员函数,则点号前面的对象是被调用成员函数的 owner 参数,否则被调用函数的 owner 参数为 null。例如 obj.func() 调用 obj 的成员函数 func,则 obj 是 obj.func 的 owner 参数。
注意一个独立的 aardio 代码文件编译后也相当于一个匿名的函数。独立运行的 aardio 代码文件 owner 参数默认为 null ,使用 import 语句加载的库文件,owner 参数为加载路径、或资源文件数据。
owner 在元方法中表示左操作数。
在迭代器函数中,owner 表示迭代目标对象。
aardio 允许用 call, callex, invoke 等函数调用其他函数并改变 owner 参数的值。