示例
涵盖语法和各个内置函数领域的可运行脚本。可以按主题筛选,或搜索其代码和说明。使用 Context* 内置函数的脚本需要由手势或热键触发。在诊断控制台中,这些内置函数会返回其空的默认值。
你好,控制台
最小的脚本。UtilityPrint 会写入诊断控制台;如果脚本不是从控制台运行的,则写入打印日志。
UtilityPrint("Hello from Input.Observer");
五种值类型
每种类型各一个变量,通过 StringFormat 打印。这是将非 Text 值转换为 Text 的唯一方法。
count = 3; // Integer
ratio = 0.75; // Real
name = "Notepad"; // Text
enabled = true; // Bool
w = WindowGetActive(); // Window
UtilityPrint(StringFormat("count={0} ratio={1}", count, ratio)); // count=3 ratio=0.750000
UtilityPrint(StringFormat("name={0} enabled={1}", name, enabled)); // name=Notepad enabled=true
UtilityPrint(StringFormat("window handle={0}", w, ""));
每种类型的真值
if 对每种类型认定为真的情况:非零数字、非空 Text、非空句柄。
if (0) UtilityPrint("never");
if (-1) UtilityPrint("-1 is true");
if (0.0) UtilityPrint("never");
if ("") UtilityPrint("never");
if (" ") UtilityPrint("a single space is true");
if ("false") UtilityPrint("the text \"false\" is true -- it's not empty");
if (!WindowFindByTitle("no window has this title zzz")) UtilityPrint("null handle is false");
else-if 链
判断光标的水平位置位于其所在显示器的哪个三分之一区域。
x = MouseGetCursorX();
y = MouseGetCursorY();
left = DisplayMonitorGetRectXFromPoint(x, y, false);
width = DisplayMonitorGetRectWidthFromPoint(x, y, false);
offset = x - left;
if (offset < width / 3) {
zone = "left";
} else if (offset < width * 2 / 3) {
zone = "center";
} else {
zone = "right";
}
UtilityPrint("Cursor is in the " + zone + " third");
计数循环:递增、递减和按步长
for 的三个部分都可以是您自选的赋值或表达式。
sum = 0;
for (i = 1; i <= 100; i = i + 1) sum = sum + i;
UtilityPrint(StringFormat("1..100 = {0}", sum, "")); // 5050
for (i = 5; i > 0; i = i - 1) UtilityPrint(StringFormat("{0}...", i, ""));
for (p = 1; p <= 1024; p = p * 2) UtilityPrint(StringFormat("{0}", p, "")); // powers of two
嵌套循环:乘法表
嵌套的 for 循环,每行构造一行 Text。
for (r = 1; r <= 5; r = r + 1) {
line = "";
for (c = 1; c <= 5; c = c + 1) {
line = line + StringFormat("{0}\t", r * c, "");
}
UtilityPrint(line);
}
while 循环:等待窗口出现,带超时
条件承担“while”的工作,计数器将等待时间限制在 5 秒以内。
ShellRun("notepad.exe");
w = WindowFindByTitle("Notepad");
for (tries = 0; !w && tries < 50; tries = tries + 1) {
UtilityWait(100);
w = WindowFindByTitle("Notepad");
}
if (w) {
WindowCenterToScreen(w);
} else {
UtilityPrint("Notepad didn't appear within 5 seconds");
}
带退出标志的 while (true)
自赋值的步进部分(go = go)构成一个只有在循环体清除标志时才会停止的循环。“全部停止”热键也总能结束它。
rolls = 0;
for (go = true; go; go = go) {
rolls = rolls + 1;
if (MathRandom(1, 6) == 6) go = false;
}
UtilityPrint(StringFormat("Rolled a six after {0} tries", rolls, ""));
break 和 continue
将计数器设为超过上限的值即可实现 break。将循环体的其余部分放入 if 中即可实现 continue。
n = StringSplit("keep,,skip-me,keep too,STOP,never reached", ",");
for (i = 0; i < n; i = i + 1) {
part = StringGetSplitPartAt(i);
if (part == "STOP") {
i = n; // break
} else if (part && !StringStartsWith(part, "skip")) { // continue for empty / skip-*
UtilityPrint(part);
}
}
意外的优先级
两个 C 语言的优先级陷阱,以及修正它们的显式括号。
flags = 6;
a = flags & 4 == 4; // flags & (4 == 4) -> 6 & true -> 0 (Bool operand)
b = (flags & 4) == 4; // true
x = 5;
c = 0 < x < 10; // (0 < x) < 10 -> true < 10 -> false (Bool vs Integer)
d = x > 0 && x < 10; // true
UtilityPrint(StringFormat("a={0} b={1}", a, b));
UtilityPrint(StringFormat("c={0} d={1}", c, d));
&& 和 || 会对两侧都求值
两个操作数总是都会运行,当右侧有副作用或可能失败时,这一点就很重要。
StorageSetValue("calls", 0);
// Both sides run even though the left is false; the right side's
// StorageSetValue still happens.
if (false && StorageSetValue("calls", 1)) { }
UtilityPrint(StringFormat("calls = {0}", StorageGetValue("calls"), "")); // calls = 1
// Guard a call that could fail with a nested if, not &&:
text = "no colon here";
idx = StringGetIndexOf(text, ":");
if (idx >= 0) {
UtilityPrint(StringGetSubstring(text, idx + 1, 100));
}
不同类型之间的相等比较
Integer 和 Real 按数值比较。其他任何两种不同类型都直接不相等。
UtilityPrint(StringFormat("5 == 5.0 -> {0}", 5 == 5.0, "")); // true
UtilityPrint(StringFormat("1 == true -> {0}", 1 == true, "")); // false
UtilityPrint(StringFormat("\"5\" == 5 -> {0}", "5" == 5, "")); // false
UtilityPrint(StringFormat("\"a\" == \"A\" -> {0}", "a" == "A", "")); // false (case-sensitive)
w = WindowFindByTitle("zzz no such window");
UtilityPrint(StringFormat("w == 0 -> {0}, !w -> {1}", w == 0, !w)); // false, true
混合类型算术回退为 0
运算符无法理解的组合会得到 Integer 0,且不报错。当结果看起来不对时,请打印该值。
a = "Total: " + 5; // 0 -- not "Total: 5"
b = true + 1; // 0
c = -"text"; // 0
d = 2.5 & 1; // 0 -- bitwise needs Integers
e = "Total: " + StringFormat("{0}", 5, ""); // "Total: 5"
UtilityPrint(StringFormat("{0} {1}", a, b) + StringFormat(" {0} {1} ", c, d) + e);
注释、空语句和块
行注释和块注释、多余的分号,以及一个顶层块(它不会创建作用域)。
// line comment
/* block comment
spanning lines */
x = 1;; // the extra ';' is an empty statement
{
y = x + 1; /* blocks group statements... */
}
UtilityPrint(StringFormat("y = {0}", y, "")); // ...but y is still visible here
Integer 与 Real 除法,以及除以零
整数除法向零截断,有一个 Real 操作数时结果为 Real,除以零得到 0。
a = 7 / 2; // 3
b = 7 / 2.0; // 3.5
c = 7 * 1.0 / 2; // 3.5 -- convert first, then divide
d = -7 / 2; // -3 (toward zero)
e = 5 / 0; // 0, no error
UtilityPrint(StringFormat("{0} {1}", a, b) + StringFormat(" {0} {1}", c, d) + StringFormat(" {0}", e, ""));
不用 % 求余数
n - (n / d) * d 就是 C 语言的 %。当除数是 2 的幂时,对非负数使用掩码也能达到同样效果。
n = 17;
d = 5;
rem = n - (n / d) * d; // 2
isEven = (n & 1) == 0; // false
low3 = n & 7; // 17 % 8 == 1
UtilityPrint(StringFormat("17 % 5 = {0}, even = {1}", rem, isEven));
UtilityPrint(StringFormat("17 % 8 = {0}", low3, ""));
舍入和 Real 数学内置函数
在内置函数声明为 Real 的位置传入 Integer 时,它会变为 Real。舍入时中间值远离零舍入。
n = 16;
root = MathSqrt(n); // 4.0 -- n is widened to 16.0
UtilityPrint(StringFormat("round 2.5 -> {0}", MathRound(2.5), "")); // 3
UtilityPrint(StringFormat("round -2.5 -> {0}", MathRound(-2.5), "")); // -3
UtilityPrint(StringFormat("floor -1.5 -> {0}", MathFloor(-1.5), "")); // -2
UtilityPrint(StringFormat("ceil 1.01 -> {0}", MathCeil(1.01), "")); // 2
UtilityPrint(StringFormat("sqrt 16 -> {0}", MathRound(root), "")); // 4
将值限制在范围内
MathClamp(与 MathMin/MathMax 一样)接受任一数值类型,并按原样返回胜出的值。
v = 300;
v = MathClamp(v, 0, 255); // 255
dist = MathAbs(-42); // 42 (Integer stays Integer)
UtilityPrint(StringFormat("clamped={0} abs={1}", v, dist));
随机数和抛硬币
MathRandom 包含范围的两端。
die = MathRandom(1, 6);
if (MathRandom(0, 1) == 1) side = "heads"; else side = "tails";
UtilityPrint(StringFormat("die={0}, coin={1}", die, side));
手势笔画的长度
累加相邻手势点之间的直线距离。平方和是 Integer,传入 MathSqrt 时会变为 Real。
n = ContextGetPointCount();
total = 0.0;
for (i = 1; i < n; i = i + 1) {
dx = ContextGetPointX(i) - ContextGetPointX(i - 1);
dy = ContextGetPointY(i) - ContextGetPointY(i - 1);
total = total + MathSqrt(dx * dx + dy * dy);
}
UtilityPrint(StringFormat("{0} points, {1} px long", n, MathRound(total)));
让鼠标沿圆周移动
角度转弧度、对 Real 进行三角运算,再用 MathRound 转回 Integer 像素坐标。
cx = MouseGetCursorX();
cy = MouseGetCursorY();
radius = 80.0;
for (deg = 0; deg <= 360; deg = deg + 10) {
rad = deg * MathGetPi() / 180;
MouseMoveTo(cx + MathRound(radius * MathCos(rad)), cy + MathRound(radius * MathSin(rad)));
UtilityWait(15);
}
MouseMoveTo(cx, cy);
格式化 Real 而不显示六位小数
缩放并舍入即可得到百分比,或将值拆分为整数部分和百分位部分以显示两位小数。适用于非负值。
ratio = 2.0 / 3.0;
pct = MathRound(ratio * 100); // 67
value = 3.14159;
cents = MathRound(value * 100); // 314
whole = cents / 100; // 3
frac = cents - whole * 100; // 14
if (frac < 10) fracText = StringFormat("0{0}", frac, ""); else fracText = StringFormat("{0}", frac, "");
UtilityPrint(StringFormat("{0}% and {1}", pct, StringFormat("{0}.", whole, "") + fracText)); // 67% and 3.14
标志掩码:设置、清除、切换、测试
对您自己的标志值执行的四种标准掩码操作。
BOLD = 0x1;
ITALIC = 0x2;
UNDERLINE = 0x4;
style = BOLD | UNDERLINE; // 0b101 = 5
style = style | ITALIC; // set -> 7
style = style & ~BOLD; // clear -> 6
style = style ^ UNDERLINE; // toggle -> 2
if ((style & ITALIC) != 0) UtilityPrint("italic is on");
if ((style & BOLD) == 0) UtilityPrint("bold is off");
UtilityPrint(StringFormat("style = {0}", style, ""));
读取光标下的像素颜色
用移位和掩码拆解 0xRRGGBB Integer。
c = DisplayGetPixelColorFromPoint(MouseGetCursorX(), MouseGetCursorY());
if (c < 0) {
UtilityPrint("The cursor isn't over any monitor");
} else {
r = (c >> 16) & 0xFF;
g = (c >> 8) & 0xFF;
b = c & 0xFF;
UtilityPrint(StringFormat("R={0} G={1}", r, g) + StringFormat(" B={0}", b, ""));
if (r > 200 && g < 80 && b < 80) UtilityPrint("That's red");
}
Integer 转十六进制文本
没有内置函数能格式化十六进制,因此每次取下四位,并在一个 Text 中查找对应的数字。
digits = "0123456789ABCDEF";
value = 48879;
hex = "";
for (v = value; v > 0; v = v >> 4) {
hex = StringGetSubstring(digits, v & 0xF, 1) + hex;
}
if (!hex) hex = "0";
UtilityPrint("0x" + hex); // 0xBEEF
统计置位的位数
依次移过全部 64 位,并累加每次的最低位。
v = 0xB6; // 1011 0110
bits = 0;
for (i = 0; i < 64; i = i + 1) {
bits = bits + ((v >> i) & 1);
}
UtilityPrint(StringFormat("0xB6 has {0} bits set", bits, "")); // 5
移位的边界情况
>> 保留符号,超出 0–63 的移位量得到 0。
UtilityPrint(StringFormat("1 << 10 = {0}", 1 << 10, "")); // 1024
UtilityPrint(StringFormat("-16 >> 2 = {0}", -16 >> 2, "")); // -4
UtilityPrint(StringFormat("1 << 64 = {0}", 1 << 64, "")); // 0
UtilityPrint(StringFormat("~0 = {0}", ~0, "")); // -1
交换两个 Integer
常规方法需要一个临时变量,而异或交换不需要。
a = 12; b = 34;
t = a; a = b; b = t; // with a temporary
a = a ^ b; b = a ^ b; a = a ^ b; // XOR swap back
UtilityPrint(StringFormat("a={0} b={1}", a, b)); // a=12 b=34
按键状态位
KeyboardGetKeyState 返回原始的 Win32 值:最高位表示按键已按下,最低位表示按键已切换。Is* 内置函数会替您完成同样的测试。
caps = KeyboardGetKeyState(VirtualKey.CapsLock);
shift = KeyboardGetKeyState(VirtualKey.ShiftKey);
UtilityPrint(StringFormat("Caps Lock on: {0}, Shift held: {1}", (caps & 1) != 0, (shift & 0x8000) != 0));
UtilityPrint(StringFormat("same via builtins: {0}, {1}",
KeyboardIsKeyToggled(VirtualKey.CapsLock), KeyboardIsKeyDown(VirtualKey.ShiftKey)));
字符串转义序列和 Windows 路径
路径中的每个反斜杠都要写两遍。\t、\n 和 \r 是转义序列,其他任何反斜杠都会被丢弃。
good = "C:\\Temp\\report.txt"; // C:\Temp\report.txt
bad = "C:\Temp\new.txt"; // C:<no backslash>Temp<line break>ew.txt
quote = "He said \"stop\"";
tabs = "Name\tValue";
cont = "one logical line, \
written on two";
UtilityPrint(good);
UtilityPrint(bad);
UtilityPrint(quote);
UtilityPrint(tabs);
UtilityPrint(cont);
格式化两个以上的值
StringFormat 只填充 {0} 和 {1}。可以嵌套调用,或用 + 连接多个结果。
w = WindowGetActive();
x = WindowGetX(w); y = WindowGetY(w);
width = WindowGetWidth(w); height = WindowGetHeight(w);
// nest: the inner call leaves "{0},{1}" for the outer call to fill
a = StringFormat(StringFormat("{0}x{1} at ", width, height) + "{0},{1}", x, y);
// concatenate: each call formats its own piece
b = StringFormat("pos {0},{1}", x, y) + StringFormat(" size {0}x{1}", width, height);
UtilityPrint(a);
UtilityPrint(b);
拆分并遍历
StringSplit 返回数量,StringGetSplitPartAt 读取每个部分。两个相邻分隔符之间的部分为空。
n = StringSplit("red,green,,blue", ",");
for (i = 0; i < n; i = i + 1) {
UtilityPrint(StringFormat("{0}: [{1}]", i, StringGetSplitPartAt(i)));
}
// 0: [red] 1: [green] 2: [] 3: [blue]
嵌套拆分:key=value 键值对
内层拆分会替换外层拆分的各部分,因此请在每一轮开始时重新拆分外层列表。
config = "width=800;height=600;title=Main";
n = StringSplit(config, ";");
for (i = 0; i < n; i = i + 1) {
StringSplit(config, ";"); // restore the outer parts...
pair = StringGetSplitPartAt(i); // ...and copy this one out
StringSplit(pair, "="); // now the cache holds key, value
UtilityPrint(StringFormat("{0} -> {1}", StringGetSplitPartAt(0), StringGetSplitPartAt(1)));
}
最后一次出现的位置:文件扩展名
没有 LastIndexOf,因此检查每个位置并保留最后一个匹配项。
file = "report.final.v2.pdf";
last = -1;
len = StringGetLength(file);
for (i = 0; i < len; i = i + 1) {
if (StringGetSubstring(file, i, 1) == ".") last = i;
}
if (last >= 0) {
ext = StringGetSubstring(file, last + 1, len - last - 1);
base = StringGetSubstring(file, 0, last);
UtilityPrint(StringFormat("base={0} ext={1}", base, ext)); // base=report.final.v2 ext=pdf
}
读取用户键入的数字
false 按用户的区域格式读取(德国用户会键入 1.234,5)。此时数字分组必须符合该格式:对于中文(简体,中国)用户,1,234.5 是数字,而 1,5 不是。StringIsNumber 能识别出不是数字的文本,而单独使用 StringToNumber 会把这类文本变成 0。
text = UIShowInputBox("Width in pixels:", "Resize", "800");
if (!StringIsNumber(text, false)) {
UIShowMessageBox("Not a number: " + text, "Resize", "OK", "Warning");
} else {
width = StringToNumber(text, false); // Integer 800, or a Real if they typed a fraction
shown = StringFromNumber(width * 1.5, 1, false); // "1,200.0" in en-US, "1.200,0" in de-DE
UtilityPrint("one and a half times that: " + shown);
}
启动程序、等待其窗口出现并对其操作
WindowWaitFor 会在超时时间(最长 60 秒)内轮询,如果没有任何匹配,则返回空窗口。模式是针对标题的不区分大小写的正则表达式;程序的标题会随 Windows 显示语言而变化,因此请匹配不会变化的那部分。
ShellRun("notepad.exe");
w = WindowWaitFor("notepad$", 10000);
if (w) {
WindowSetActive(w);
KeyboardTypeText("Notes for today:\n");
WindowWaitClose(w, 60000); // carries on once the user closes it, or after a minute
UtilityPrint("Notepad is gone");
} else {
UtilityPrint("Notepad didn't open in 10 seconds");
}
用正则表达式从复制的文本中提取值
采用 PCRE2 语法,除非模式以 (?i) 开头,否则区分大小写,并且支持 Unicode(\w 匹配带重音的字母)。组 0 是整个匹配;模式中的反斜杠在脚本字符串中写作 \\。替换文本中的 $1 表示组 1。Window*Regex/Shell*Regex 内置函数使用同一个引擎,但始终不区分大小写。
text = ClipboardGetText();
if (StringRegexMatch(text, "(?i)order\\s*#?(\\d+)")) {
UtilityPrint("order number: " + StringRegexGetGroupAt(1));
} else {
UtilityPrint("no order number on the clipboard");
}
// every run of spaces, tabs and line breaks becomes one space; "#1234" becomes "order 1234"
text = StringRegexReplace(text, "\\s+", " ");
ClipboardSetText(StringRegexReplace(text, "#(\\d+)", "order $1"));
数字补零
在前面添加字符,直到 Text 足够长。
n = 7;
s = StringFormat("{0}", n, "");
for (s = s; StringGetLength(s) < 3; s = s) {
s = "0" + s;
}
UtilityPrint(s); // 007
反转 Text
按 UTF-16 单元从后向前遍历 Text。基本多文种平面之外的字符(大多数表情符号)占两个单元,会被拆开。
text = "Input.Observer";
rev = "";
for (i = StringGetLength(text) - 1; i >= 0; i = i - 1) {
rev = rev + StringGetSubstring(text, i, 1);
}
UtilityPrint(rev); // revresbO.tupnI
统计剪贴板中的单词数
将制表符和换行变为空格,按空格拆分,然后统计非空的部分。
text = ClipboardGetText();
text = StringReplace(text, "\n", " ");
text = StringReplace(text, "\t", " ");
n = StringSplit(text, " ");
words = 0;
for (i = 0; i < n; i = i + 1) {
if (StringTrim(StringGetSplitPartAt(i))) words = words + 1;
}
UtilityPrint(StringFormat("{0} words", words, ""));
不区分大小写的比较
将两侧都转换为小写,因为所有 Text 运算符和内置函数都区分大小写。
title = WindowGetTitle(WindowGetActive());
if (StringContains(StringToLower(title), "visual studio")) {
UtilityPrint("In Visual Studio");
}
exe = WindowGetExecutableName(WindowGetActive());
if (StringToLower(exe) == "notepad.exe") UtilityPrint("In Notepad");
Text 排序按序数进行
< 比较 UTF-16 代码单元:所有大写 ASCII 字母都排在所有小写字母之前。
UtilityPrint(StringFormat("\"apple\" < \"banana\" : {0}", "apple" < "banana", "")); // true
UtilityPrint(StringFormat("\"Zebra\" < \"apple\" : {0}", "Zebra" < "apple", "")); // true
UtilityPrint(StringFormat("\"apple\" < \"Banana\" : {0}", "apple" < "Banana", "")); // false
a = "apple"; b = "Banana";
UtilityPrint(StringFormat("case-folded : {0}", StringToLower(a) < StringToLower(b), "")); // true
填充模板并粘贴
StringReplace 填充 %NAME% 占位符。粘贴前后会保存并还原剪贴板。
template = "Hi %NAME%,\n\nThanks for the report on %APP%.\n";
text = StringReplace(template, "%NAME%", "Sam");
text = StringReplace(text, "%APP%", ContextGetApplicationName());
ClipboardSave();
ClipboardPasteReplacementText(text);
UtilityWait(150); // let the target app read the clipboard first
ClipboardRestore();
将剪贴板中的多行合并为一行
按 \n 拆分。StringTrim 还会去除 Windows 行结束符中的 \r,因此无论各行以 \r\n 还是仅以 \n 结尾,这种方法都适用。
n = StringSplit(ClipboardGetText(), "\n");
out = "";
for (i = 0; i < n; i = i + 1) {
part = StringTrim(StringGetSplitPartAt(i));
if (part) {
if (out) out = out + " ";
out = out + part;
}
}
ClipboardSetText(out);
在网上搜索所选文本
ClipboardCopySelection 会等待复制完成,而不是靠猜测。StringUrlEncode 可防止 &、#、空格和带重音的字母破坏地址。只对搜索词进行编码,切勿对整个 URL 编码。
ClipboardSave();
term = StringTrim(ClipboardCopySelection(1000));
ClipboardRestore();
if (term) ShellRun("https://www.bing.com/search?q=" + StringUrlEncode(term));
一个手势,多个选项
UIShowMenu 每行接受一个菜单项(单独一个 - 表示分隔线),并返回所选菜单项的索引(从 0 开始计数,跳过分隔线);如果菜单被关闭,则返回 -1。UIShowInputBox 在点击“取消”时返回 ""。
choice = UIShowMenu("Maximize\nMinimize\n-\nRename window...");
w = ContextGetWindow();
if (choice == 0) WindowMaximize(w);
else if (choice == 1) WindowMinimize(w);
else if (choice == 2) {
title = UIShowInputBox("New title for this window:", "Rename", WindowGetTitle(w));
if (title) WindowSetTitle(w, title);
}
今天的日期,以及带时间戳的文件名
DateTimeGetNow 返回 ISO 8601 文本。DateTimeFormat 可以按用户习惯的格式输出它,也可以输出为 FileStamp,后者可按时间排序,且可安全用于文件名。
now = DateTimeGetNow();
KeyboardTypeText(DateTimeFormat(now, DateTimeStyle.LongDate));
path = ShellExpandEnvironmentVariables("%USERPROFILE%\\Desktop\\note-") + DateTimeFormat(now, DateTimeStyle.FileStamp) + ".txt";
FileCreate(path, ClipboardGetText());
if (DateTimeGetPart(now, DateTimePart.Hour) >= 17) UtilityPrint("after hours");
命名常量与原始数字
常量会编译为对应的数字,因此这些调用是完全相同的。十六进制与 Win32 文档一致。
KeyboardPressKey(VirtualKey.VolumeMute);
KeyboardPressKey(0xAD); // the same key, as a hex VK code
KeyboardPressKey(173); // and in decimal
UtilityPrint(StringFormat("VirtualKey.A = {0}, MouseButton.Right = {1}", VirtualKey.A, MouseButton.Right));
切换手势所在窗口的最大化状态
最常见的手势操作。ContextGetWindow 是手势开始时所在的窗口。
w = ContextGetWindow();
if (WindowIsMaximized(w)) WindowRestore(w); else WindowMaximize(w);
将窗口置顶
WindowSetTopmost 显式设置始终置顶,WindowToggleTopmost 则切换该状态。窗口属性会记住此脚本所选的状态。
w = ContextGetWindow();
pinned = WindowGetPropertyInteger(w, "io.pinned") == 1;
WindowSetTopmost(w, !pinned);
if (pinned) {
WindowRemoveProp(w, "io.pinned");
} else {
WindowSetPropertyInteger(w, "io.pinned", 1);
ShellShowToast("Pinned on top", WindowGetTitle(w));
}
循环切换窗口透明度
将不透明度从 255 逐步降到 105,再回到 255。非分层窗口报告的值为 255。
w = ContextGetWindow();
a = WindowGetAlpha(w) - 50;
if (a < 100) a = 255;
WindowSetAlpha(w, a);
将活动窗口贴靠到其所在显示器的左半边
使用工作区(true),这样窗口就不会遮住任务栏。
w = WindowGetActive();
cx = WindowGetX(w) + WindowGetWidth(w) / 2;
cy = WindowGetY(w) + WindowGetHeight(w) / 2;
x = DisplayMonitorGetRectXFromPoint(cx, cy, true);
y = DisplayMonitorGetRectYFromPoint(cx, cy, true);
width = DisplayMonitorGetRectWidthFromPoint(cx, cy, true);
height = DisplayMonitorGetRectHeightFromPoint(cx, cy, true);
WindowRestore(w);
WindowMoveTo(w, x, y);
WindowResizeTo(w, width / 2, height);
将窗口贴靠到光标下方的 3×2 网格单元格中
Region* 内置函数将矩形划分为单元格,查找包含某个点的单元格,并给出其边界。
mx = MouseGetCursorX();
my = MouseGetCursorY();
x = DisplayMonitorGetRectXFromPoint(mx, my, true);
y = DisplayMonitorGetRectYFromPoint(mx, my, true);
w = DisplayMonitorGetRectWidthFromPoint(mx, my, true);
h = DisplayMonitorGetRectHeightFromPoint(mx, my, true);
cell = RegionGetCellIndexAt(x, y, w, h, 3, 2, mx, my);
win = ContextGetWindow();
if (cell >= 0 && win) {
WindowRestore(win);
WindowMoveTo(win, RegionGetX(x, y, w, h, 3, 2, cell), RegionGetY(x, y, w, h, 3, 2, cell));
WindowResizeTo(win, RegionGetWidth(x, y, w, h, 3, 2, cell), RegionGetHeight(x, y, w, h, 3, 2, cell));
}
将窗口移到下一个显示器
第二个参数让鼠标随窗口一起移动。
WindowSendToNextScreen(ContextGetWindow(), true);
列出可见的顶层窗口
先枚举再按索引读取,跳过隐藏、遮盖和无标题的窗口。
n = WindowGetAllTopLevel();
shown = 0;
for (i = 0; i < n; i = i + 1) {
w = WindowGetEnumeratedAt(i);
if (WindowGetVisible(w)) {
if (!WindowIsCloaked(w)) {
title = WindowGetTitle(w);
if (title) {
UtilityPrint(StringFormat("{0} [{1}]", title, WindowGetExecutableName(w)));
shown = shown + 1;
}
}
}
}
UtilityPrint(StringFormat("{0} of {1} windows", shown, n));
最小化某个应用的所有窗口
用不区分大小写的正则表达式匹配可执行文件。注意 \\.:这是写在脚本字符串中的正则表达式 \.。
n = WindowFindAllByModuleRegex("notepad\\.exe$");
for (i = 0; i < n; i = i + 1) {
WindowMinimize(WindowGetEnumeratedAt(i));
}
UtilityPrint(StringFormat("Minimized {0} Notepad windows", n, ""));
确认后按标题模式关闭窗口
针对标题的正则表达式、消息中的数量,以及对所点击按钮的检查。
n = WindowFindAllByTitleRegex("^Untitled - ");
if (n > 0) {
answer = UIShowMessageBox(StringFormat("Close {0} untitled window(s)?", n, ""),
"Tidy up", "YesNo", "Question");
if (answer == "Yes") {
for (i = 0; i < n; i = i + 1) WindowClose(WindowGetEnumeratedAt(i));
}
}
记住并还原窗口位置
窗口属性将 Integer 保存在窗口自身上,在多次运行之间保留,直到窗口关闭。从未设置过的属性读取为 0。
w = ContextGetWindow();
if (WindowGetPropertyInteger(w, "io.saved") == 1) {
WindowMoveTo(w, WindowGetPropertyInteger(w, "io.x"), WindowGetPropertyInteger(w, "io.y"));
WindowRemoveProp(w, "io.saved");
WindowRemoveProp(w, "io.x");
WindowRemoveProp(w, "io.y");
} else {
WindowSetPropertyInteger(w, "io.x", WindowGetX(w));
WindowSetPropertyInteger(w, "io.y", WindowGetY(w));
WindowSetPropertyInteger(w, "io.saved", 1);
WindowCenterToScreen(w);
}
检查窗口的子控件
子窗口与顶层枚举使用同一个窗口缓存。
w = ContextGetWindow();
n = WindowGetAllChildren(w, true);
for (i = 0; i < n; i = i + 1) {
c = WindowGetEnumeratedAt(i);
UtilityPrint(StringFormat("{0}: {1}", WindowGetClassName(c), WindowGetTitle(c)));
}
从进程到窗口
按可执行文件名枚举进程 ID,然后查找每个进程的窗口。
n = ShellEnumerateProcessIdsByExeRegex("^explorer\\.exe$");
for (i = 0; i < n; i = i + 1) {
pid = ShellGetEnumeratedProcessIdAt(i);
w = WindowFromProcessId(pid);
UtilityPrint(StringFormat("pid {0}: {1}", pid, WindowGetTitle(w)));
}
描述光标下方的内容
某个点下方的控件、它的顶层窗口,以及转换为客户区坐标的该点。
x = MouseGetCursorX();
y = MouseGetCursorY();
control = WindowControlFromPoint(x, y);
top = WindowGetRoot(control);
UtilityPrint(StringFormat("control class: {0}; window: {1}", WindowGetClassName(control), WindowGetTitle(top)));
UtilityPrint(StringFormat("client point: {0},{1}",
WindowMapScreenPointToClientX(top, x, y), WindowMapScreenPointToClientY(top, x, y)));
将窗口隐藏到托盘
最小化到通知区域。TrayRestoreAllWindows() 会将所有这些窗口恢复。
TrayMinimizeWindow(ContextGetWindow());
触发上下文知道的一切
将此脚本附加到一个手势,即可查看该手势的脚本可以读取哪些内容。
UtilityPrint(StringFormat("action={0} gesture={1}", ContextGetActionName(), ContextGetGestureName()));
UtilityPrint(StringFormat("app={0} window={1}", ContextGetApplicationName(), WindowGetTitle(ContextGetWindow())));
UtilityPrint(StringFormat("box x={0} y={1}", ContextGetBoundingBoxX(), ContextGetBoundingBoxY())
+ StringFormat(" w={0} h={1}", ContextGetBoundingBoxWidth(), ContextGetBoundingBoxHeight()));
UtilityPrint(StringFormat("points={0} button={1}", ContextGetPointCount(), ContextGetStrokeButton()));
笔画朝哪个方向?
比较第一个点和最后一个点,选出变化较大的那根轴。
n = ContextGetPointCount();
if (n > 1) {
dx = ContextGetPointX(n - 1) - ContextGetPointX(0);
dy = ContextGetPointY(n - 1) - ContextGetPointY(0);
if (MathAbs(dx) > MathAbs(dy)) {
if (dx > 0) dir = "right"; else dir = "left";
} else {
if (dy > 0) dir = "down"; else dir = "up";
}
UtilityPrint("The stroke went " + dir);
}
按笔画按钮分支
ContextGetStrokeButton 返回一个 MouseButton.* 值;在手势或鼠标按钮触发器之外返回 -1。
b = ContextGetStrokeButton();
if (b == MouseButton.Secondary) {
WindowMinimize(ContextGetWindow());
} else if (b == MouseButton.Middle) {
WindowClose(ContextGetWindow());
} else if (b == -1) {
UtilityPrint("Not triggered by a mouse button");
}
截取您圈出的区域
将手势的边界框另存为“图片”文件夹中的 PNG 文件。
path = ShellExpandEnvironmentVariables("%USERPROFILE%\\Pictures\\gesture-capture.png");
ok = CaptureSaveRegion(path, ContextGetBoundingBoxX(), ContextGetBoundingBoxY(),
ContextGetBoundingBoxWidth(), ContextGetBoundingBoxHeight());
if (ok) ShellShowToast("Captured", path);
放行未识别的绘制
如果手势是在绘图应用上绘制的,则将该笔画作为普通拖动重放给该应用。
if (StringToLower(ContextGetApplicationName()) == "mspaint.exe") {
ContextRelayGesture();
}
点击某处,然后将光标放回原处
将光标位置保存在变量中,点击,然后返回。
x = MouseGetCursorX();
y = MouseGetCursorY();
MouseClick(20, 20, MouseButton.Primary);
UtilityWait(50);
MouseMoveTo(x, y);
点击窗口内的某个点
无论窗口被移到哪里,客户区坐标都保持正确。
w = WindowFindByTitle("Calculator");
if (w) {
WindowSetActive(w);
MouseClickAtClientPoint(w, 40, 60, MouseButton.Primary);
}
脚本化拖动
按下、分步移动、释放。独立的按下、移动和释放内置函数正是为此而设。
x = MouseGetCursorX();
y = MouseGetCursorY();
MouseButtonDown(MouseButton.Primary);
for (i = 1; i <= 20; i = i + 1) {
MouseMoveTo(x + i * 10, y);
UtilityWait(10);
}
MouseButtonUp(MouseButton.Primary);
按刻度滚动
滚轮的一个刻度是 120。负值表示向下或向左滚动。
MouseScrollVertical(-3 * 120); // three notches down
MouseScrollHorizontal(120); // one notch right
将光标限制在窗口内 5 秒
MouseLockToRect 在整个系统范围内生效,直到调用 MouseUnlock 为止,因此请始终成对使用它们。
w = ContextGetWindow();
MouseLockToRect(WindowGetX(w), WindowGetY(w), WindowGetWidth(w), WindowGetHeight(w));
UtilityWait(5000);
MouseUnlock();
组合键
修饰键前缀 ^ 表示 Ctrl、+ 表示 Shift、% 表示 Alt、@ 表示 Win,后跟一个键。
KeyboardPressKeyCombo("^c"); // Ctrl+C
KeyboardPressKeyCombo("^+{ESC}"); // Ctrl+Shift+Esc
KeyboardPressKeyCombo("@{LEFT}"); // Win+Left
KeyboardPressKeyCombo("%{F4}"); // Alt+F4
键入签名
KeyboardTypeText 键入字符。在行与行之间以组合键的方式发送 Enter,这样每个应用都能收到一次真正的 Enter 按键。
KeyboardTypeText("Best regards,");
KeyboardPressKeyCombo("{ENTER}");
KeyboardTypeText("Sam");
Shift+单击
在鼠标操作前后按住一个键,并在同一个脚本中释放它。未释放的键会一直保持按下状态。
KeyboardKeyDown(VirtualKey.ShiftKey);
MouseClick(MouseGetCursorX(), MouseGetCursorY(), MouseButton.Primary);
KeyboardKeyUp(VirtualKey.ShiftKey);
媒体键
按 VirtualKey 名称发送媒体键和音量键。
KeyboardPressKey(VirtualKey.MediaPlayPause);
UtilityWait(200);
KeyboardPressKey(VirtualKey.MediaNextTrack);
按住 Ctrl 时改变行为
检查实时的按键状态,让一个手势具有两种含义。
w = ContextGetWindow();
if (KeyboardIsKeyDown(VirtualKey.ControlKey)) {
WindowClose(w);
} else {
WindowMinimize(w);
}
将所选文本转为大写
复制所选内容,修改后粘贴回去,并还原剪贴板中原有的内容。
ClipboardSave();
ClipboardClear();
KeyboardPressKeyCombo("^c");
UtilityWait(100);
text = ClipboardGetText();
if (text) {
ClipboardPasteReplacementText(StringToUpper(text));
UtilityWait(100);
}
ClipboardRestore();
在网上搜索所选内容
复制所选内容并打开一个搜索 URL。空格会变为 +。要同时编码其他所有字符,请像在网上搜索所选文本那样使用 StringUrlEncode。
ClipboardSave();
KeyboardPressKeyCombo("^c");
UtilityWait(100);
q = StringTrim(ClipboardGetText());
ClipboardRestore();
if (q) {
ShellRun("https://www.bing.com/search?q=" + StringReplace(q, " ", "+"));
}
将复制的图像保存到文件
将剪贴板中的图像写入磁盘并报告结果。
path = ShellExpandEnvironmentVariables("%USERPROFILE%\\Desktop\\clipboard.png");
if (ClipboardSaveImage(path)) ShellShowToast("Saved", path);
else UtilityPrint("No image on the clipboard");
追加到日志文件
第一次运行时创建带标题行的文件,之后每次运行追加一行。
log = ShellExpandEnvironmentVariables("%TEMP%\\input-observer-gestures.log");
if (!FileExists(log)) FileCreate(log, "gesture\tapplication\n");
FileAppendText(log, StringFormat("{0}\t{1}\n", ContextGetGestureName(), ContextGetApplicationName()));
读取文件并统计行数
FileReadText 以 Text 形式返回整个文件,然后按 \n 拆分。
path = "C:\\Windows\\System32\\drivers\\etc\\hosts";
text = FileReadText(path);
n = StringSplit(text, "\n");
comments = 0;
for (i = 0; i < n; i = i + 1) {
if (StringStartsWith(StringTrim(StringGetSplitPartAt(i)), "#")) comments = comments + 1;
}
UtilityPrint(StringFormat("{0} lines, {1} comments", n, comments));
UtilityPrint(StringFormat("{0} bytes, modified {1}", FileGetSize(path), FileGetModifiedDate(path)));
统计文件夹中的文件类型
不递归地枚举一个文件夹,并以不区分大小写的方式统计匹配项。
folder = ShellExpandEnvironmentVariables("%USERPROFILE%\\Downloads");
n = FolderEnumerateAll(folder, false);
pdf = 0; zip = 0;
for (i = 0; i < n; i = i + 1) {
p = StringToLower(FolderGetEnumeratedPathAt(i));
if (StringEndsWith(p, ".pdf")) pdf = pdf + 1;
else if (StringEndsWith(p, ".zip")) zip = zip + 1;
}
UtilityPrint(StringFormat("{0} PDFs, {1} ZIPs", pdf, zip) + StringFormat(" of {0} entries", n, ""));
编辑前备份文件
以覆盖方式复制,然后替换内容。
path = ShellExpandEnvironmentVariables("%USERPROFILE%\\notes.txt");
if (FileExists(path)) {
FileCopy(path, path + ".bak", true);
FileEditText(path, StringToUpper(FileReadText(path)));
}
监视文件夹
用 | 组合 FileNotify 标志。监视的脚本是 Text,因此其内部的引号需要转义。它通过 ContextGetWatch* 读取发生了什么。
FolderWatchCreate("downloads",
ShellExpandEnvironmentVariables("%USERPROFILE%\\Downloads"),
false,
FileNotify.FileName | FileNotify.LastWrite,
"UtilityPrint(ContextGetWatchAction() + \": \" + ContextGetWatchPath());");
// later: FolderWatchDelete("downloads");
会计数的重复计时器
计时器的脚本有自己的变量,因此由 Storage 在各次触发之间传递计数。每隔一秒触发一次,共 5 次。
StorageSetValue("ticks", 0);
TimerCreate("counter", 1000, 1000, 5,
"t = StorageGetValue(\"ticks\") + 1; StorageSetValue(\"ticks\", t); UtilityPrint(StringFormat(\"tick {0}\", t, \"\"));");
来自代码片段的计时器脚本,无需转义
将较长的计时器脚本保存在代码片段中并传入其文本,这样就没有需要转义的嵌套引号。
// Fires once, 10 minutes from now (repeat count 1).
TimerCreate("stretch-reminder", 600000, 1000, 1, SnippetGetScript("Stretch reminder"));
列出并停止计时器
枚举命名计时器,并删除其中一个或全部。
n = TimerEnumerateAll();
for (i = 0; i < n; i = i + 1) UtilityPrint(TimerGetEnumeratedNameAt(i));
TimerDelete("counter");
// TimerDeleteAll();
在多次运行之间保持的开关
对从未设置过的键调用 StorageGetValue 会得到 Integer 0,即假值,因此第一次运行会打开该模式。该值会一直保留到 Input.Observer 退出;Persistent 内置函数可以让值在重启后仍然保留。
on = !StorageGetValue("quietMode");
StorageSetValue("quietMode", on);
MultimediaSetMute(AudioEndpoint.Playback, on);
if (on) ShellShowToast("Quiet mode", "On"); else ShellShowToast("Quiet mode", "Off");
重启后仍保留的计数器
Storage*Persistent* 内置函数将值保存在配置旁边的 storage.toml 中,因此 Input.Observer 重启后这些值仍然存在。它是纯文本,从不加密:不要在其中保存密码。Window 无法保存;句柄在重启后没有任何意义。如果 Input.Observer 启动时 storage.toml 存在但无法读取,StorageSetPersistentValue 会返回 false:该值只保留到 Input.Observer 退出,在下次启动之前不会保存任何内容。
n = StorageGetPersistentValue("gestures.used") + 1; // 0 the first time ever
StorageSetPersistentValue("gestures.used", n);
UtilityPrint(StringFormat("{0} gestures since this counter started", n, ""));
保存在 Storage 中的列表
没有数组,但用 StringFormat 构造的编号键可以起到同样的作用。
count = StorageGetValue("recent.count");
StorageSetValue(StringFormat("recent.{0}", count, ""), WindowGetTitle(ContextGetWindow()));
StorageSetValue("recent.count", count + 1);
for (i = 0; i <= count; i = i + 1) {
UtilityPrint(StorageGetValue(StringFormat("recent.{0}", i, "")));
}
每次只让一个操作运行某段代码
命名锁可以防止两次重叠的运行交错执行。请将结果与 LockResult 比较,并在完成后释放锁。
r = UtilityLockAcquire("clipboard-work", 5);
if (r == LockResult.Acquired) {
ClipboardSave();
// ...work that must not overlap another run...
ClipboardRestore();
UtilityLockRelease("clipboard-work");
} else {
UtilityPrint("Another action is busy with the clipboard");
}
提出问题
按钮和图标是 Text 名称。结果是所点击按钮的名称。
answer = UIShowMessageBox("Save the layout before closing?", "Input.Observer", "YesNoCancel", "Question");
if (answer == "Yes") {
UtilityPrint("saving");
} else if (answer == "No") {
UtilityPrint("discarding");
} else {
UtilityPrint("cancelled");
}
调高音量并显示屏幕提示
音量是 0.0 到 1.0 之间的 Real。UIShowDisplayMessage 接受 19 个按位置传递的参数。
v = MathMin(MultimediaGetVolume(AudioEndpoint.Playback) + 0.05, 1.0);
MultimediaSetVolume(AudioEndpoint.Playback, v);
UIShowDisplayMessage(
"Volume", StringFormat("{0}%", MathRound(v * 100), ""),
1200, // durationMs
0.9, // opacity (Real)
Location.BottomCenter, // or a "x,y" Text point
"Segoe UI", 14, true, false, // title font, size, bold, italic
"Segoe UI", 12, false, false, // message font, size, bold, italic
"#1F1F1F", "#F7F7F5", // fore / back color: name, #RRGGBB or "R,G,B"
12, // padding
false, // usePrimaryScreen
TextAlign.Center, TextAlign.Center);
实时更新的显示消息
保留会话 ID,并将其连同同样的 19 个参数传给 UIUpdateDisplayMessage。
id = UIShowDisplayMessage("Working", "0%", 10000, 0.95, Location.TopRight,
"Segoe UI", 12, true, false, "Segoe UI", 11, false, false,
"black", "255,255,255", 10, true, TextAlign.Left, TextAlign.Left);
for (p = 0; p <= 100; p = p + 20) {
UIUpdateDisplayMessage(id, "Working", StringFormat("{0}%", p, ""), 10000, 0.95, Location.TopRight,
"Segoe UI", 12, true, false, "Segoe UI", 11, false, false,
"black", "255,255,255", 10, true, TextAlign.Left, TextAlign.Left);
UtilityWait(400);
}
UICloseDisplayMessage(id);
Windows 通知
一个标题和一条消息,以 Toast 通知的形式显示。
ShellShowToast("Input.Observer", StringFormat("Profile: {0}", GestureProfileGetActiveId(), ""));
切换麦克风静音
Capture 终结点是默认的录音设备。
MultimediaToggleMute(AudioEndpoint.Capture);
if (MultimediaGetMute(AudioEndpoint.Capture)) ShellShowToast("Microphone", "Muted");
else ShellShowToast("Microphone", "Live");
播放声音
播放指定路径的 WAV 和 MP3 文件。
MultimediaPlayWavFile("C:\\Windows\\Media\\chimes.wav");
启动或切换到某个应用
如果应用已在运行,则激活它;否则启动它。
ShellRunOrActivate("notepad.exe");
使用谓词和窗口样式运行程序
谓词是 ShellVerb 常量或以 Text 形式给出的任何谓词。窗口样式是 WindowStyle 常量。
ShellRunProgram("notepad.exe", "C:\\Temp\\todo.txt", ShellVerb.Open, WindowStyle.Maximized, false);
ShellRunProgram("C:\\Temp\\report.pdf", "", "print", WindowStyle.Hidden, false); // Text verb
运行并等待退出
将 waitForExit 设为 true 时,脚本的线程会一直阻塞,直到程序关闭。在此期间输入照常传递。停止所有操作会结束等待:程序继续运行,调用返回 false。
ShellRunProgram("notepad.exe", "", ShellVerb.Open, WindowStyle.Normal, true);
ShellShowToast("Notepad", "was closed");
展开环境变量
构造一个在每个用户的计算机上都能使用的路径。
UtilityPrint(ShellExpandEnvironmentVariables("%APPDATA%\\Input.Observer"));
UtilityPrint(ShellExpandEnvironmentVariables("%COMPUTERNAME% / %USERNAME%"));
交给 AutoHotkey 处理
在进程外运行 AutoHotkey 代码并返回其退出代码;如果未配置 AutoHotkey,则返回 -1。
rc = AutoHotkeyExecuteScript("SoundBeep 750, 200");
if (rc == -1) UtilityPrint("AutoHotkey isn't set up");
列出显示器
按索引列出每个显示器的名称、完整边界、工作区和 DPI。
n = DisplayMonitorEnumeratedAll();
for (i = 0; i < n; i = i + 1) {
UtilityPrint(StringFormat("#{0} {1}", i, DisplayMonitorGetEnumeratedFriendlyNameAt(i)));
UtilityPrint(StringFormat(" bounds {0} x {1}", DisplayMonitorGetEnumeratedWidthAt(i, false), DisplayMonitorGetEnumeratedHeightAt(i, false))
+ StringFormat(", work area {0} x {1}", DisplayMonitorGetEnumeratedWidthAt(i, true), DisplayMonitorGetEnumeratedHeightAt(i, true))
+ StringFormat(", {0} dpi", DisplayMonitorGetEnumeratedDpiAt(i), ""));
}
将窗口发送到指定的显示器
按枚举中的索引发送;如果该显示器不存在,则改为发送到下一个屏幕。
w = ContextGetWindow();
if (DisplayMonitorEnumeratedAll() > 1) {
WindowSendToMonitorIndex(w, 1, false);
} else {
WindowSendToNextScreen(w, false);
}
切换到下一个手势配置方案
找到当前配置方案的索引,加一,然后用求余的惯用写法回绕。
n = GestureProfileEnumerateAll();
if (n > 0) {
active = GestureProfileGetActiveId();
next = 0;
for (i = 0; i < n; i = i + 1) {
if (GestureProfileGetEnumeratedIdAt(i) == active) next = i + 1;
}
next = next - (next / n) * n; // next % n
GestureProfileSwitch(GestureProfileGetEnumeratedIdAt(next));
ShellShowToast("Gesture profile", GestureProfileGetEnumeratedNameAt(next));
}
引擎状态
读取引擎是否已禁用或处于安全模式,然后暂停它。可从托盘菜单重新启用。
UtilityPrint(StringFormat("disabled={0} safeMode={1}", EngineIsDisabled(), EngineIsSafeMode()));
if (!EngineIsDisabled()) {
EngineDisable();
ShellShowToast("Input.Observer", "Paused. Re-enable it from the tray menu.");
}
放行下一次右键拖动
跳过一次手势捕获,使下一次右键拖动传递给应用。
EngineDisableNextGesture();
将代码片段用作可重用函数
通过 Storage 传递参数和结果。代码片段会运行完毕,然后才执行下一行。
// Snippet "Tile window" reads tile.index and positions ContextGetWindow() accordingly.
StorageSetValue("tile.index", 2);
SnippetExecuteScript("Tile window");
UtilityPrint(StringFormat("snippet reported: {0}", StorageGetValue("tile.result"), ""));
与插件通信
向已连接的插件发送一条 Text 命令,并在超时时间内等待其 Text 回复。
reply = PluginSendMessage("MyRenderer", "theme dark", 2);
UtilityPrint("Plugin replied: " + reply);
向串口设备提问
打开端口,写入一行,最多等待 2 秒接收回复,然后关闭端口。校验位和停止位使用常量。
if (SerialOpenPort("COM3", 9600, SerialParity.None, 8, SerialStopBits.One, "\n")) {
SerialWriteTextLine("COM3", "PING", 9600);
reply = SerialGetTextLine("COM3", 2, 9600);
SerialClosePort("COM3");
if (reply) UtilityPrint("Device: " + reply); else UtilityPrint("No reply");
}
将串口设备的按钮映射到媒体键
监视器会为接收到的每一行运行其脚本。ContextGetSerialTextLine 即为该行。
SerialMonitorCreate("pad", "COM3", 115200, SerialParity.None, 8, SerialStopBits.One, "\n",
"line = StringTrim(ContextGetSerialTextLine()); if (line == \"B1\") KeyboardPressKey(VirtualKey.MediaPlayPause); else if (line == \"B2\") KeyboardPressKey(VirtualKey.MediaNextTrack);");
列出 COM 端口
列出当前存在的端口,例如 Arduino 刚插入时获得的端口。请参阅串口设备和 Arduino。
n = SerialEnumeratePorts();
if (n == 0) UtilityPrint("No COM ports. Is the board plugged in?");
for (i = 0; i < n; i = i + 1) {
UtilityPrint(SerialGetEnumeratedPortAt(i));
}
保持 Arduino 的端口打开并向其发送命令
只打开一次端口,这样打开端口时会重启的开发板就不会错过命令,然后发送一行并读取回复。
// Load global event: open the port once, at the sketch's speed. An Uno or Nano
// restarts every time its port opens, so a port opened per command would lose
// the command while the board boots.
if (!SerialOpenPort("COM4", 115200, SerialParity.None, 8, SerialStopBits.One, "")) {
UtilityPrint("COM4 isn't available: unplugged, or open in the Arduino IDE's Serial Monitor?");
}
// Any gesture or hotkey: send a command line and read the board's reply.
// (The sketch reads a line with Serial.readStringUntil('\n') and answers OK.)
if (SerialWriteTextLine("COM4", "LED 255 0 0", 115200)) {
reply = SerialGetTextLine("COM4", 1, 115200);
if (reply != "OK") UtilityPrint(StringFormat("Board said: {0}", reply, ""));
}
将 Arduino 旋钮变成音量控制
每一行中 0 到 1023 的电位器读数会变为播放音量。
// Arduino side: a potentiometer on A0, one reading per line.
// void setup() { Serial.begin(115200); }
// void loop() { Serial.println(analogRead(A0)); delay(50); }
// Empty text as the terminator means CR LF, which Serial.println sends.
SerialMonitorCreate("knob", "COM4", 115200, SerialParity.None, 8, SerialStopBits.One, "",
"line = ContextGetSerialTextLine(); if (StringIsNumber(line, true)) MultimediaSetVolume(AudioEndpoint.Playback, StringToNumber(line, true) / 1023.0);");