first commit
This commit is contained in:
commit
761b95080f
404 files changed
+312608
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
.vs/
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 2026-10-23
|
||||||
|
|
||||||
|
## 按键精灵 v2:全 C# 重做(Lua + 积木 + 录制 + 录屏)
|
||||||
|
|
||||||
|
用户反馈 v1(自定义 VB-like 脚本 + TreeView 假积木)未达预期,明确要求「整个项目用 C」——
|
||||||
|
即纯 C# 技术栈,不要网页方案。中途一度做了 WebView2 + Blockly,被要求停止并移除。
|
||||||
|
|
||||||
|
### 架构决策
|
||||||
|
- 脚本引擎:MoonSharp 2.0.0(纯托管,.NET 10 可用),替换自研 ScriptParser/ScriptEngine。
|
||||||
|
- 积木:`Forms/BlockCanvas.cs` 自绘 GDI+ 画布,圆角块、拖拽排序、容器嵌套、插入指示线、右键菜单。
|
||||||
|
- 录制:`Capture/InputRecorder.cs` WH_KEYBOARD_LL/WH_MOUSE_LL → 智能聚合成 Lua。
|
||||||
|
- 录屏:`Capture/ScreenRecorder.cs` GDI 抓帧 + JPEG + 手写 AVI(MJPG) 封装,无 ffmpeg 依赖。
|
||||||
|
- 删除旧 `Scripting/` 目录(ScriptParser/ScriptEngine/Blocks/AutomationTask)与 `Forms/BlockDesigner.cs`。
|
||||||
|
|
||||||
|
### 关键实现坑
|
||||||
|
- MoonSharp `Table["name"] = Delegate` 可直接赋值;CLR 回调抛异常会原样透传给 `DoString`,`pcall` 拦不住 → 用作 Abort 信号。
|
||||||
|
- `CoreModules` 枚举成员是 `String`/`Table`/`Math`(不是 StrLib/TableLib)。
|
||||||
|
- WinForms `UserControl.Text` 会遮蔽自定义属性;`LuaEditor` 改用 `Script` 属性。
|
||||||
|
- `SplitContainer` 构造期设 `SplitterDistance` 会因宽度未定抛异常,改在 `Layout` 里初始化。
|
||||||
|
- 积木模板里参数不要再手写引号(ArgKind 已负责加引号),否则 `keyPress(""A"")`。
|
||||||
|
- IDE 规则:`MainForm` 实现 ILuaHost 时与 `Control` 同名方法必须显式接口实现。
|
||||||
|
|
||||||
|
### 验收
|
||||||
|
- `dotnet build` 0 错误。
|
||||||
|
- 积木→Lua 生成、Lua 执行、语法错误中文提示、录屏 AVI 结构、主窗体 5 页渲染均已实测通过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.1 界面重构:四区布局(左任务列表 / 中详情 / 右插件 / 下日志)
|
||||||
|
|
||||||
|
用户要求按经典按键精灵布局重排,不要一堆平级标签页。
|
||||||
|
|
||||||
|
### 结构
|
||||||
|
- 左 248px:`ListBox` + `OwnerDrawFixed` 自绘任务卡(名称 + 循环/间隔 + 状态色条),右键新建/复制/删除/上下移/立即运行。
|
||||||
|
- 中:顶部任务属性条(名称/启用/无限/次数/间隔/来源/热键,改动实时回写) + `TabControl` 四页(脚本代码 / 积木编程 / 录制·录屏 / 内嵌浏览器)。
|
||||||
|
- 右 224px:插件面板 `FlowLayoutPanel`,17 项分 4 组(屏幕与定位 / 录制 / 脚本 / 系统与帮助),按钮宽 176 才不触发横向滚动条。
|
||||||
|
- 下:RichTextBox 日志跨全宽,错误红/成功绿,分割条可拖。
|
||||||
|
- 嵌套 `SplitContainer` ×3,均在 `Layout` 事件里一次性设 `SplitterDistance`。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- `Forms/PickerForm.cs`:置顶坐标取色器,15×15 抓屏 7 倍 NearestNeighbor 放大 + 准星,实时坐标/HEX/RGB,复制坐标/颜色/写日志。
|
||||||
|
- `LuaTask` 增加 `Blocks`(积木 JSON)与 `Hotkey` 字段,任务切换时脚本与积木各自恢复。
|
||||||
|
- 插件:找图测试(全屏 MatchTemplate + 耗时/相似度)、窗口列表、打开图库/录像目录。
|
||||||
|
|
||||||
|
### 坑
|
||||||
|
- `ListBox.DataSource = BindingList<T>` 在句柄创建前绑定,`Items.Count` 恒为 0 → 改为手工 `Items.Clear/Add` 同步 + `_sync` 标志防重入。
|
||||||
|
- `Label.AutoSize=true` 在 `FlowLayoutPanel` 里会被压到 100px 宽截断文字 → 改 `AutoSize=false` + 固定宽 + `AutoEllipsis`。
|
||||||
|
- `Form.MainMenuStrip` 会把菜单塞到 Controls[0](最内层),工具栏 `Dock=Top` 会压到菜单上方 → 末尾 `Controls.SetChildIndex(menu, Count-1)` 强制菜单最外。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.2 积木插片耦合造型 + 功能补位
|
||||||
|
|
||||||
|
用户反馈:积木要"插片耦合"样式(可理解)、各功能位置没做好。
|
||||||
|
|
||||||
|
### BlockCanvas 重写(Forms/BlockCanvas.cs)
|
||||||
|
- 几何:RowH=30 / NotchH=7 / WallW=22 / FootH=16 / ElseH=22;`TopEdge/BottomEdge/PuzzlePath/ContainerPath` 生成路径。
|
||||||
|
- 语句块 = 顶部凹槽 + 底部凸榫;相邻块 y 累加 RowH(无间隙)→ 凸榫嵌进凹槽,视觉咬合。
|
||||||
|
- 容器 = C 形路径(头 + 左壁 + 内腔 + 封口),内腔画浅色插槽底;空容器注册 Slot 行显示"把积木拖到这里"。
|
||||||
|
- 否则 = 内腔横向分隔条(整宽、块色加深)。
|
||||||
|
- 参数值 = 白色圆角插槽(SplitTitle 把 Title 按 {i} 拆段交替绘制固定文字/参数槽)。
|
||||||
|
- 命中用 _rows 倒序;End 行 Owner=末尾列表;Slot 行 Owner=空列表 → 拖入空容器可用。
|
||||||
|
|
||||||
|
### 坐标 bug(重要)
|
||||||
|
容器子块起点原为 innerTop+NotchH,而 InnerHeight 不含该偏移 → 子块总高比内腔多 7px,最后一块盖住底部封口。修复:子块从 innerTop 开始,elseY 同步去掉偏移。
|
||||||
|
|
||||||
|
### 主窗体补位
|
||||||
|
- 任务绑定启动热键:属性条 ComboBox(无/F1~F7)→ RebindTaskHotkeys() 注册 HK_TASK_BASE=200 起的动态 id,WndProc 派发 RunTaskByHotkey。
|
||||||
|
- 任务列表右侧画启用勾选框,点击直接切换(e.Y % 48 >= 16 判定行内区域)。
|
||||||
|
- 插件分组标题带类别色(▍ 前缀)。
|
||||||
|
- 积木页命令库 198 / 参数 230,画布 ~450px。
|
||||||
|
- 坑:构建时 exe 被残留进程锁定 → 先 taskkill 再 build。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.3 脚本⇄积木双向转换 + 显示修复
|
||||||
|
|
||||||
|
用户反馈:截图圈了三处——属性条按钮被截、积木顶部被截、脚本和积木不该是两份代码。
|
||||||
|
|
||||||
|
### 双向转换(核心)
|
||||||
|
- 单一数据源 = task.Script。BlockCatalog.FromLua()(LuaBlocks.cs):行解析器,支持 for/while/if/elseif(嵌套进 else)/else/end、ifExists(do+local __ix 三行结构还原)、varSet/varAdd、目录模板匹配(TplCache:按函数名+返回值数+参数占位符构建,顶层逗号 SplitArgs 尊重引号括号)、认不出→ Key="lua" 自定义代码块(Template="{0}" Raw)。
|
||||||
|
- ToLua 去掉了日期头注释,否则往返不稳定。
|
||||||
|
- MainForm:_converting 互斥标志;切积木页 SelectedIndexChanged→SyncScriptToBlocks;_canvas.Changed→回写 _editor.Script;参数面板 TextBox/Combo 改动→_canvas.NotifyChanged()(新增公开方法,原 Changed 只在结构变化时发,改参数不触发是坑);LoadTaskDetail 旧数据迁移(只有 Blocks 无 Script 时转脚本);OpenBlocks 载入后同步转脚本。
|
||||||
|
|
||||||
|
### 显示修复
|
||||||
|
- 详情属性条 head 78→88、row2 28→34(按钮曾被截半)。
|
||||||
|
- 积木页工具条 WrapContents=true、Height=74(8 个按钮一行放不下)。
|
||||||
|
- BlockCanvas.LoadBlocks 加 AutoScrollPosition 归零(旧滚动位置把首块顶出视野)。
|
||||||
|
- 插件栏 224→234、按钮/分组标题 176→186。
|
||||||
|
- 坑:用户开着旧 exe 锁文件,taskkill 后再 build。
|
||||||
|
|
||||||
|
|
||||||
|
### v2.3 追修
|
||||||
|
- 用户再报:按钮底部仍被页签盖住 → head 88→96、row2 34→42 / Padding(2,2,0,0),按钮 28+m3 在 30 容器里溢出是根因。
|
||||||
|
- 用户报"没互相转换"→ LoadTaskDetail 改为任务载入后**无条件** SyncScriptToBlocks()(原来只靠 SelectedIndexChanged,且启动首载时画布为空)。实测三流程全通:启动切积木页 18 块、改参数回写 delay(999)、脚本加行切页出现 TAB 块。
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using Microsoft.Web.WebView2.Core;
|
||||||
|
using Microsoft.Web.WebView2.WinForms;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace anjianjingling.Browser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 内嵌 WebView2 命令桥:把脚本命令同步封送到 UI 线程执行。
|
||||||
|
/// 脚本引擎在后台线程,WebView2 必须在 UI 线程访问。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WebViewBridge
|
||||||
|
{
|
||||||
|
private readonly WebView2 _web;
|
||||||
|
private const int DefaultTimeoutMs = 30000;
|
||||||
|
|
||||||
|
public WebViewBridge(WebView2 web) { _web = web; }
|
||||||
|
|
||||||
|
private T Invoke<T>(Func<Task<T>> func, int timeoutMs = DefaultTimeoutMs)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<T>();
|
||||||
|
_web.BeginInvoke(async () =>
|
||||||
|
{
|
||||||
|
try { tcs.TrySetResult(await func()); }
|
||||||
|
catch (Exception ex) { tcs.TrySetException(ex); }
|
||||||
|
});
|
||||||
|
if (!tcs.Task.Wait(timeoutMs))
|
||||||
|
throw new TimeoutException("浏览器操作超时");
|
||||||
|
return tcs.Task.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvokeAction(Func<Task> func, int timeoutMs = DefaultTimeoutMs)
|
||||||
|
{
|
||||||
|
Invoke(async () => { await func(); return true; }, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>ExecuteScriptAsync 返回的是 JSON 字符串,解码为可读文本。</summary>
|
||||||
|
private static string DecodeJsResult(string json)
|
||||||
|
{
|
||||||
|
if (json == "null" || string.IsNullOrEmpty(json)) return "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||||||
|
return el.ValueKind == JsonValueKind.String
|
||||||
|
? el.GetString() ?? ""
|
||||||
|
: el.GetRawText();
|
||||||
|
}
|
||||||
|
catch { return json; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Js(string code) => JsonSerializer.Serialize(code);
|
||||||
|
|
||||||
|
// ===================== 命令 =====================
|
||||||
|
|
||||||
|
public void Open(string url)
|
||||||
|
{
|
||||||
|
Invoke(async () =>
|
||||||
|
{
|
||||||
|
var nav = new TaskCompletionSource<bool>();
|
||||||
|
void Handler(object? s, CoreWebView2NavigationCompletedEventArgs e)
|
||||||
|
{
|
||||||
|
_web.NavigationCompleted -= Handler;
|
||||||
|
nav.TrySetResult(e.IsSuccess);
|
||||||
|
}
|
||||||
|
_web.NavigationCompleted += Handler;
|
||||||
|
_web.CoreWebView2.Navigate(url);
|
||||||
|
bool ok = await nav.Task;
|
||||||
|
return ok;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExecJs(string js)
|
||||||
|
=> DecodeJsResult(Invoke(() => _web.CoreWebView2.ExecuteScriptAsync(js)));
|
||||||
|
|
||||||
|
public bool Click(string selector)
|
||||||
|
{
|
||||||
|
string js = $$"""
|
||||||
|
(()=>{
|
||||||
|
const el=document.querySelector({{Js(selector)}});
|
||||||
|
if(!el) return false;
|
||||||
|
el.scrollIntoView({block:'center'});
|
||||||
|
el.dispatchEvent(new MouseEvent('mousedown',{bubbles:true}));
|
||||||
|
el.dispatchEvent(new MouseEvent('mouseup',{bubbles:true}));
|
||||||
|
el.click();
|
||||||
|
return true;
|
||||||
|
})()
|
||||||
|
""";
|
||||||
|
return ExecJs(js) == "True";
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Type(string selector, string text)
|
||||||
|
{
|
||||||
|
string js = $$"""
|
||||||
|
(()=>{
|
||||||
|
const el=document.querySelector({{Js(selector)}});
|
||||||
|
if(!el) return false;
|
||||||
|
const proto = el.tagName==='TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
||||||
|
const setter = Object.getOwnPropertyDescriptor(proto,'value').set;
|
||||||
|
setter.call(el, {{Js(text)}});
|
||||||
|
el.dispatchEvent(new Event('input',{bubbles:true}));
|
||||||
|
el.dispatchEvent(new Event('change',{bubbles:true}));
|
||||||
|
return true;
|
||||||
|
})()
|
||||||
|
""";
|
||||||
|
return ExecJs(js) == "True";
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Exists(string selector)
|
||||||
|
=> ExecJs($"!!document.querySelector({Js(selector)})") == "True";
|
||||||
|
|
||||||
|
public bool WaitFor(string selector, int timeoutMs)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||||
|
{
|
||||||
|
try { if (Exists(selector)) { tcs.TrySetResult(true); return; } }
|
||||||
|
catch { }
|
||||||
|
await Task.Delay(200);
|
||||||
|
}
|
||||||
|
tcs.TrySetResult(false);
|
||||||
|
});
|
||||||
|
return tcs.Task.Wait(timeoutMs + 5000) && tcs.Task.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetText(string selector)
|
||||||
|
{
|
||||||
|
string js = $$"""
|
||||||
|
(()=>{
|
||||||
|
const el=document.querySelector({{Js(selector)}});
|
||||||
|
if(!el) return null;
|
||||||
|
return el.value!==undefined ? el.value : (el.innerText||el.textContent);
|
||||||
|
})()
|
||||||
|
""";
|
||||||
|
return ExecJs(js);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Shot(string path)
|
||||||
|
{
|
||||||
|
InvokeAction(async () =>
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
|
||||||
|
await using var fs = new FileStream(path, FileMode.Create);
|
||||||
|
await _web.CoreWebView2.CapturePreviewAsync(
|
||||||
|
CoreWebView2CapturePreviewImageFormat.Png, fs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace anjianjingling.Capture
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作录制器:WH_KEYBOARD_LL / WH_MOUSE_LL 低级钩子采集真实键鼠事件,
|
||||||
|
/// 结束时聚合为可直接回放的 Lua 脚本。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InputRecorder
|
||||||
|
{
|
||||||
|
// ===================== Win32 =====================
|
||||||
|
private const int WH_KEYBOARD_LL = 13;
|
||||||
|
private const int WH_MOUSE_LL = 14;
|
||||||
|
|
||||||
|
private const int WM_KEYDOWN = 0x0100, WM_KEYUP = 0x0101;
|
||||||
|
private const int WM_SYSKEYDOWN = 0x0104, WM_SYSKEYUP = 0x0105;
|
||||||
|
private const int WM_MOUSEMOVE = 0x0200;
|
||||||
|
private const int WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202;
|
||||||
|
private const int WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205;
|
||||||
|
private const int WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208;
|
||||||
|
private const int WM_MOUSEWHEEL = 0x020A;
|
||||||
|
private const int WM_QUIT = 0x0012;
|
||||||
|
|
||||||
|
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct KBDLLHOOKSTRUCT
|
||||||
|
{
|
||||||
|
public uint vkCode;
|
||||||
|
public uint scanCode;
|
||||||
|
public uint flags;
|
||||||
|
public uint time;
|
||||||
|
public IntPtr dwExtraInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct POINT { public int X, Y; }
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MSLLHOOKSTRUCT
|
||||||
|
{
|
||||||
|
public POINT pt;
|
||||||
|
public uint mouseData;
|
||||||
|
public uint flags;
|
||||||
|
public uint time;
|
||||||
|
public IntPtr dwExtraInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MSG
|
||||||
|
{
|
||||||
|
public IntPtr hwnd;
|
||||||
|
public uint message;
|
||||||
|
public IntPtr wParam;
|
||||||
|
public IntPtr lParam;
|
||||||
|
public uint time;
|
||||||
|
public POINT pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool TranslateMessage(ref MSG lpMsg);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr DispatchMessage(ref MSG lpMsg);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern uint GetCurrentThreadId();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool GetKeyboardState(byte[] lpKeyState);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern short GetKeyState(int nVirtKey);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState,
|
||||||
|
[Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr GetKeyboardLayout(uint idThread);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int GetKeyboardLayoutList(int nBuff, [Out] IntPtr[]? lpList);
|
||||||
|
|
||||||
|
// ===================== 事件模型 =====================
|
||||||
|
private enum Kind { Move, ButtonDown, ButtonUp, Wheel, KeyDown, KeyUp }
|
||||||
|
|
||||||
|
private sealed class RecEvent
|
||||||
|
{
|
||||||
|
public long T;
|
||||||
|
public Kind Kind;
|
||||||
|
public int X, Y;
|
||||||
|
public string Btn = "left";
|
||||||
|
public int Vk;
|
||||||
|
public string KeyName = "";
|
||||||
|
public string? Ch;
|
||||||
|
public int Wheel;
|
||||||
|
|
||||||
|
public bool IsPrintable => Ch is { Length: > 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<RecEvent> _events = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private long _t0;
|
||||||
|
private Thread? _thread;
|
||||||
|
private uint _threadId;
|
||||||
|
private IntPtr _kbHook, _msHook;
|
||||||
|
private HookProc? _kbProc, _msProc;
|
||||||
|
private readonly ManualResetEventSlim _ready = new(false);
|
||||||
|
private volatile bool _recording;
|
||||||
|
|
||||||
|
public bool IsRecording => _recording;
|
||||||
|
public event Action<bool>? StateChanged;
|
||||||
|
|
||||||
|
/// <summary>录制鼠标移动轨迹(关闭后只记录点击)。</summary>
|
||||||
|
public bool RecordMouseMove { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>把连续可打印字符聚合成 typeText("...")。</summary>
|
||||||
|
public bool SmartText { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>生成脚本时单次延时上限(毫秒),超过则截断。</summary>
|
||||||
|
public int MaxDelayMs { get; set; } = 2000;
|
||||||
|
|
||||||
|
/// <summary>录制期间忽略的虚拟键(默认吞掉本程序热键 F8~F12)。</summary>
|
||||||
|
public HashSet<int> IgnoredVk { get; } = new() { 0x77, 0x78, 0x79, 0x7A, 0x7B };
|
||||||
|
|
||||||
|
// ===================== 开始 / 停止 =====================
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_recording) return;
|
||||||
|
lock (_lock) _events.Clear();
|
||||||
|
_t0 = Environment.TickCount64;
|
||||||
|
_recording = true;
|
||||||
|
|
||||||
|
_thread = new Thread(HookThread)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "InputRecorderHook"
|
||||||
|
};
|
||||||
|
_thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
_thread.Start();
|
||||||
|
_ready.Wait(2000);
|
||||||
|
StateChanged?.Invoke(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HookThread()
|
||||||
|
{
|
||||||
|
_threadId = GetCurrentThreadId();
|
||||||
|
_kbProc = KbProc;
|
||||||
|
_msProc = MsProc;
|
||||||
|
IntPtr hMod = GetModuleHandle(null);
|
||||||
|
_kbHook = SetWindowsHookEx(WH_KEYBOARD_LL, _kbProc, hMod, 0);
|
||||||
|
_msHook = SetWindowsHookEx(WH_MOUSE_LL, _msProc, hMod, 0);
|
||||||
|
_ready.Set();
|
||||||
|
|
||||||
|
while (GetMessage(out MSG msg, IntPtr.Zero, 0, 0) > 0)
|
||||||
|
{
|
||||||
|
TranslateMessage(ref msg);
|
||||||
|
DispatchMessage(ref msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_kbHook != IntPtr.Zero) UnhookWindowsHookEx(_kbHook);
|
||||||
|
if (_msHook != IntPtr.Zero) UnhookWindowsHookEx(_msHook);
|
||||||
|
_kbHook = _msHook = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr KbProc(int nCode, IntPtr wParam, IntPtr lParam)
|
||||||
|
{
|
||||||
|
if (nCode >= 0 && _recording)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var kb = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
|
||||||
|
int msg = wParam.ToInt32();
|
||||||
|
bool down = msg is WM_KEYDOWN or WM_SYSKEYDOWN;
|
||||||
|
bool up = msg is WM_KEYUP or WM_SYSKEYUP;
|
||||||
|
if ((down || up) && !IgnoredVk.Contains((int)kb.vkCode))
|
||||||
|
{
|
||||||
|
var e = new RecEvent
|
||||||
|
{
|
||||||
|
T = Environment.TickCount64 - _t0,
|
||||||
|
Kind = down ? Kind.KeyDown : Kind.KeyUp,
|
||||||
|
Vk = (int)kb.vkCode,
|
||||||
|
KeyName = KeyName((int)kb.vkCode)
|
||||||
|
};
|
||||||
|
if (down)
|
||||||
|
{
|
||||||
|
var ch = ToChar((int)kb.vkCode, (int)kb.scanCode);
|
||||||
|
if (ch.HasValue && !CtrlAltDown()) e.Ch = ch.Value.ToString();
|
||||||
|
}
|
||||||
|
lock (_lock) _events.Add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return CallNextHookEx(_kbHook, nCode, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr MsProc(int nCode, IntPtr wParam, IntPtr lParam)
|
||||||
|
{
|
||||||
|
if (nCode >= 0 && _recording)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ms = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
|
||||||
|
int msg = wParam.ToInt32();
|
||||||
|
var e = new RecEvent
|
||||||
|
{
|
||||||
|
T = Environment.TickCount64 - _t0,
|
||||||
|
X = ms.pt.X,
|
||||||
|
Y = ms.pt.Y
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (msg)
|
||||||
|
{
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
if (!RecordMouseMove) break;
|
||||||
|
e.Kind = Kind.Move;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// 采样节流:同位置或 15ms 内的移动丢弃
|
||||||
|
if (_events.Count > 0)
|
||||||
|
{
|
||||||
|
var last = _events[^1];
|
||||||
|
if (last.Kind == Kind.Move)
|
||||||
|
{
|
||||||
|
if (e.T - last.T < 15 && Math.Abs(e.X - last.X) < 3 && Math.Abs(e.Y - last.Y) < 3)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_events.Add(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
case WM_RBUTTONDOWN:
|
||||||
|
case WM_MBUTTONDOWN:
|
||||||
|
e.Kind = Kind.ButtonDown;
|
||||||
|
e.Btn = msg == WM_RBUTTONDOWN ? "right" : msg == WM_MBUTTONDOWN ? "middle" : "left";
|
||||||
|
lock (_lock) _events.Add(e);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONUP:
|
||||||
|
case WM_RBUTTONUP:
|
||||||
|
case WM_MBUTTONUP:
|
||||||
|
e.Kind = Kind.ButtonUp;
|
||||||
|
e.Btn = msg == WM_RBUTTONUP ? "right" : msg == WM_MBUTTONUP ? "middle" : "left";
|
||||||
|
lock (_lock) _events.Add(e);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_MOUSEWHEEL:
|
||||||
|
e.Kind = Kind.Wheel;
|
||||||
|
e.Wheel = (short)((ms.mouseData >> 16) & 0xFFFF) / 120;
|
||||||
|
lock (_lock) _events.Add(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return CallNextHookEx(_msHook, nCode, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>停止录制并生成 Lua 脚本。</summary>
|
||||||
|
public string Stop()
|
||||||
|
{
|
||||||
|
if (!_recording) return "";
|
||||||
|
_recording = false;
|
||||||
|
StateChanged?.Invoke(false);
|
||||||
|
|
||||||
|
if (_threadId != 0) PostThreadMessage(_threadId, WM_QUIT, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
_thread?.Join(1500);
|
||||||
|
_thread = null;
|
||||||
|
_threadId = 0;
|
||||||
|
|
||||||
|
List<RecEvent> snapshot;
|
||||||
|
lock (_lock) snapshot = new List<RecEvent>(_events);
|
||||||
|
return Generate(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abort()
|
||||||
|
{
|
||||||
|
if (!_recording) return;
|
||||||
|
_recording = false;
|
||||||
|
StateChanged?.Invoke(false);
|
||||||
|
if (_threadId != 0) PostThreadMessage(_threadId, WM_QUIT, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
_thread?.Join(1500);
|
||||||
|
_thread = null;
|
||||||
|
_threadId = 0;
|
||||||
|
lock (_lock) _events.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 键名 / 字符 =====================
|
||||||
|
|
||||||
|
private static bool CtrlAltDown()
|
||||||
|
=> (GetKeyState(0x11) & 0x8000) != 0 || (GetKeyState(0x12) & 0x8000) != 0;
|
||||||
|
|
||||||
|
private static char? ToChar(int vk, int scan)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var state = new byte[256];
|
||||||
|
if (!GetKeyboardState(state)) return null;
|
||||||
|
var sb = new StringBuilder(8);
|
||||||
|
var layout = GetKeyboardLayout(0);
|
||||||
|
int n = ToUnicodeEx((uint)vk, (uint)scan, state, sb, sb.Length, 0, layout);
|
||||||
|
if (n == 1 && sb.Length > 0)
|
||||||
|
{
|
||||||
|
char c = sb[0];
|
||||||
|
if (c >= 32 && c != 127) return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, string> VkNames = new()
|
||||||
|
{
|
||||||
|
[0x0D] = "ENTER", [0x1B] = "ESC", [0x20] = "SPACE", [0x09] = "TAB",
|
||||||
|
[0x08] = "BACKSPACE", [0x2E] = "DELETE", [0x2D] = "INSERT",
|
||||||
|
[0x24] = "HOME", [0x23] = "END", [0x21] = "PAGEUP", [0x22] = "PAGEDOWN",
|
||||||
|
[0x25] = "LEFT", [0x26] = "UP", [0x27] = "RIGHT", [0x28] = "DOWN",
|
||||||
|
[0x11] = "CTRL", [0x12] = "ALT", [0x10] = "SHIFT", [0x5B] = "WIN",
|
||||||
|
[0x14] = "CAPSLOCK", [0x90] = "NUMLOCK",
|
||||||
|
[0x6A] = "MULTIPLY", [0x6B] = "ADD", [0x6D] = "SUBTRACT", [0x6E] = "DECIMAL", [0x6F] = "DIVIDE",
|
||||||
|
[0xBA] = "SEMICOLON", [0xBB] = "EQUAL", [0xBC] = "COMMA", [0xBD] = "MINUS",
|
||||||
|
[0xBE] = "PERIOD", [0xBF] = "SLASH", [0xC0] = "BACKQUOTE",
|
||||||
|
[0xDB] = "LBRACKET", [0xDC] = "BACKSLASH", [0xDD] = "RBRACKET", [0xDE] = "QUOTE",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string KeyName(int vk)
|
||||||
|
{
|
||||||
|
if (VkNames.TryGetValue(vk, out var n)) return n;
|
||||||
|
if (vk >= 0x70 && vk <= 0x87) return "F" + (vk - 0x6F);
|
||||||
|
if (vk >= 0x60 && vk <= 0x69) return "NUMPAD" + (vk - 0x60);
|
||||||
|
if (vk >= 0x30 && vk <= 0x39) return ((char)vk).ToString();
|
||||||
|
if (vk >= 0x41 && vk <= 0x5A) return ((char)vk).ToString();
|
||||||
|
return "VK" + vk.ToString("X2");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsModifier(int vk) => vk is 0x11 or 0x12 or 0x10 or 0x5B or 0x5C
|
||||||
|
or 0xA0 or 0xA1 or 0xA2 or 0xA3 or 0xA4 or 0xA5;
|
||||||
|
|
||||||
|
private static string ModName(int vk) => vk switch
|
||||||
|
{
|
||||||
|
0x11 or 0xA2 or 0xA3 => "Ctrl",
|
||||||
|
0x12 or 0xA4 or 0xA5 => "Alt",
|
||||||
|
0x10 or 0xA0 or 0xA1 => "Shift",
|
||||||
|
0x5B or 0x5C => "Win",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===================== Lua 生成 =====================
|
||||||
|
|
||||||
|
private string Generate(List<RecEvent> ev)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("-- 由操作录制生成(" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ")");
|
||||||
|
sb.AppendLine("-- 修改坐标/延时后可直接运行;模板图片放 images/ 目录");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
long lastT = 0;
|
||||||
|
var mods = new List<string>();
|
||||||
|
var textBuf = new StringBuilder();
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
void FlushText()
|
||||||
|
{
|
||||||
|
if (textBuf.Length == 0) return;
|
||||||
|
sb.AppendLine($"typeText(\"{Escape(textBuf.ToString())}\")");
|
||||||
|
textBuf.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitDelay(long t)
|
||||||
|
{
|
||||||
|
long gap = t - lastT;
|
||||||
|
if (gap < 30) return;
|
||||||
|
if (gap > MaxDelayMs) gap = MaxDelayMs;
|
||||||
|
FlushText();
|
||||||
|
sb.AppendLine($"delay({gap})");
|
||||||
|
lastT = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < ev.Count)
|
||||||
|
{
|
||||||
|
var e = ev[i];
|
||||||
|
|
||||||
|
switch (e.Kind)
|
||||||
|
{
|
||||||
|
case Kind.Move:
|
||||||
|
{
|
||||||
|
// 合并连续移动,只保留最终落点
|
||||||
|
int j = i;
|
||||||
|
while (j + 1 < ev.Count && ev[j + 1].Kind == Kind.Move) j++;
|
||||||
|
var end = ev[j];
|
||||||
|
EmitDelay(e.T);
|
||||||
|
sb.AppendLine($"moveTo({end.X}, {end.Y})");
|
||||||
|
lastT = end.T;
|
||||||
|
i = j + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Kind.ButtonDown:
|
||||||
|
{
|
||||||
|
// 找 500ms 内的同名弹起 → 单击
|
||||||
|
int j = i + 1;
|
||||||
|
RecEvent? up = null;
|
||||||
|
while (j < ev.Count)
|
||||||
|
{
|
||||||
|
if (ev[j].Kind == Kind.ButtonUp && ev[j].Btn == e.Btn && ev[j].T - e.T < 500) { up = ev[j]; break; }
|
||||||
|
if (ev[j].Kind == Kind.ButtonDown) break;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
EmitDelay(e.T);
|
||||||
|
string fn = e.Btn switch { "right" => "rightClick", "middle" => "middleClick", _ => "leftClick" };
|
||||||
|
if (up != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{fn}({e.X}, {e.Y})");
|
||||||
|
lastT = up.T;
|
||||||
|
i = j + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sb.AppendLine($"moveTo({e.X}, {e.Y})");
|
||||||
|
sb.AppendLine($"mouse.down(\"{e.Btn}\")");
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Kind.ButtonUp:
|
||||||
|
EmitDelay(e.T);
|
||||||
|
sb.AppendLine($"mouse.up(\"{e.Btn}\")");
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Kind.Wheel:
|
||||||
|
EmitDelay(e.T);
|
||||||
|
sb.AppendLine($"wheel({e.Wheel})");
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Kind.KeyDown:
|
||||||
|
{
|
||||||
|
if (IsModifier(e.Vk))
|
||||||
|
{
|
||||||
|
string m = ModName(e.Vk);
|
||||||
|
if (m.Length > 0 && !mods.Contains(m)) mods.Add(m);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找对应弹起
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < ev.Count && !(ev[j].Kind == Kind.KeyUp && ev[j].Vk == e.Vk)) j++;
|
||||||
|
bool paired = j < ev.Count;
|
||||||
|
|
||||||
|
if (mods.Count > 0 && mods.Any(m => m is "Ctrl" or "Alt" or "Win"))
|
||||||
|
{
|
||||||
|
EmitDelay(e.T);
|
||||||
|
sb.AppendLine($"hotkey(\"{string.Join("+", mods.Append(e.KeyName))}\")");
|
||||||
|
// 跳过本次按下到弹起之间的所有事件
|
||||||
|
i = paired ? j + 1 : i + 1;
|
||||||
|
lastT = paired ? ev[j].T : e.T;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SmartText && e.IsPrintable)
|
||||||
|
{
|
||||||
|
textBuf.Append(e.Ch);
|
||||||
|
i = paired ? j + 1 : i + 1;
|
||||||
|
lastT = paired ? ev[j].T : e.T;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmitDelay(e.T);
|
||||||
|
sb.AppendLine($"keyPress(\"{e.KeyName}\")");
|
||||||
|
i = paired ? j + 1 : i + 1;
|
||||||
|
lastT = paired ? ev[j].T : e.T;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Kind.KeyUp:
|
||||||
|
// 修饰键弹起
|
||||||
|
if (IsModifier(e.Vk))
|
||||||
|
{
|
||||||
|
string m = ModName(e.Vk);
|
||||||
|
if (m.Length > 0) mods.Remove(m);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
EmitDelay(e.T);
|
||||||
|
sb.AppendLine($"keyUp(\"{e.KeyName}\")");
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushText();
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Escape(string s)
|
||||||
|
=> s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "").Replace("\n", "\\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using anjianjingling.Vision;
|
||||||
|
|
||||||
|
namespace anjianjingling.Capture
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 屏幕录像:GDI 抓帧 → JPEG 编码 → 手写 AVI(MJPG) 封装,无需 ffmpeg。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ScreenRecorder : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ScreenCapture _capture = new();
|
||||||
|
private Thread? _thread;
|
||||||
|
private volatile bool _running;
|
||||||
|
private AviWriter? _writer;
|
||||||
|
|
||||||
|
public bool IsRecording => _running;
|
||||||
|
public string? OutputPath { get; private set; }
|
||||||
|
public long FrameCount { get; private set; }
|
||||||
|
public event Action<string, long>? Finished;
|
||||||
|
|
||||||
|
public int Fps { get; set; } = 15;
|
||||||
|
public bool DrawCursor { get; set; } = true;
|
||||||
|
public int Quality { get; set; } = 70;
|
||||||
|
|
||||||
|
public void Start(string path, Rectangle region)
|
||||||
|
{
|
||||||
|
if (_running) return;
|
||||||
|
|
||||||
|
int w = region.Width & ~1; // 编码器要求偶数宽高更稳
|
||||||
|
int h = region.Height & ~1;
|
||||||
|
if (w < 16 || h < 16) throw new ArgumentException("录制区域太小");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
|
||||||
|
OutputPath = path;
|
||||||
|
FrameCount = 0;
|
||||||
|
_writer = new AviWriter(path, w, h, Math.Clamp(Fps, 1, 60));
|
||||||
|
_running = true;
|
||||||
|
|
||||||
|
var rect = new Rectangle(region.X, region.Y, w, h);
|
||||||
|
var codec = GetJpegCodec();
|
||||||
|
var ep = new EncoderParameters(1);
|
||||||
|
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)Math.Clamp(Quality, 20, 95));
|
||||||
|
|
||||||
|
_thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
int frameInterval = Math.Max(1, 1000 / Math.Clamp(Fps, 1, 60));
|
||||||
|
long frameIdx = 0;
|
||||||
|
|
||||||
|
while (_running)
|
||||||
|
{
|
||||||
|
// 固定帧率:按理论时间点抓帧,抓不动就丢帧,保证 AVI 时长等于真实时长
|
||||||
|
long target = frameIdx * frameInterval;
|
||||||
|
long now = sw.ElapsedMilliseconds;
|
||||||
|
if (now < target)
|
||||||
|
{
|
||||||
|
Thread.Sleep((int)Math.Min(30, target - now));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var bmp = _capture.CaptureRegion(rect.X, rect.Y, rect.Width, rect.Height);
|
||||||
|
if (DrawCursor)
|
||||||
|
{
|
||||||
|
using var g = Graphics.FromImage(bmp);
|
||||||
|
DrawCursorOn(g, rect.X, rect.Y);
|
||||||
|
}
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
bmp.Save(ms, codec, ep);
|
||||||
|
_writer!.AddFrame(ms.ToArray());
|
||||||
|
FrameCount++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Thread.Sleep(frameInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
frameIdx = Math.Max(frameIdx + 1, now / frameInterval + 1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{ IsBackground = true, Name = "ScreenRecorder" };
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (!_running) return;
|
||||||
|
_running = false;
|
||||||
|
_thread?.Join(3000);
|
||||||
|
_thread = null;
|
||||||
|
_writer?.Complete();
|
||||||
|
_writer?.Dispose();
|
||||||
|
_writer = null;
|
||||||
|
if (OutputPath != null) Finished?.Invoke(OutputPath, FrameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImageCodecInfo GetJpegCodec()
|
||||||
|
=> ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
|
||||||
|
|
||||||
|
// ===================== 光标 =====================
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct CURSORINFO
|
||||||
|
{
|
||||||
|
public int cbSize;
|
||||||
|
public int flags;
|
||||||
|
public IntPtr hCursor;
|
||||||
|
public POINT ptScreenPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct POINT { public int X, Y; }
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct ICONINFO
|
||||||
|
{
|
||||||
|
public bool fIcon;
|
||||||
|
public int xHotspot;
|
||||||
|
public int yHotspot;
|
||||||
|
public IntPtr hbmMask;
|
||||||
|
public IntPtr hbmColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int CURSOR_SHOWING = 0x00000001;
|
||||||
|
private const int DI_NORMAL = 0x0003;
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool GetCursorInfo(ref CURSORINFO pci);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon,
|
||||||
|
int cxWidth, int cyWidth, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")]
|
||||||
|
private static extern bool DeleteObject(IntPtr hObject);
|
||||||
|
|
||||||
|
private static void DrawCursorOn(Graphics g, int offsetX, int offsetY)
|
||||||
|
{
|
||||||
|
var ci = new CURSORINFO { cbSize = Marshal.SizeOf<CURSORINFO>() };
|
||||||
|
if (!GetCursorInfo(ref ci) || (ci.flags & CURSOR_SHOWING) == 0) return;
|
||||||
|
|
||||||
|
int hx = 0, hy = 0;
|
||||||
|
if (GetIconInfo(ci.hCursor, out var ii))
|
||||||
|
{
|
||||||
|
hx = ii.xHotspot;
|
||||||
|
hy = ii.yHotspot;
|
||||||
|
if (ii.hbmMask != IntPtr.Zero) DeleteObject(ii.hbmMask);
|
||||||
|
if (ii.hbmColor != IntPtr.Zero) DeleteObject(ii.hbmColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr hdc = g.GetHdc();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DrawIconEx(hdc, ci.ptScreenPos.X - offsetX - hx, ci.ptScreenPos.Y - offsetY - hy,
|
||||||
|
ci.hCursor, 0, 0, 0, IntPtr.Zero, DI_NORMAL);
|
||||||
|
}
|
||||||
|
finally { g.ReleaseHdc(hdc); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最小可用 AVI(MJPG) 封装:RIFF/hdrl(avih+strl)/movi/idx1。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class AviWriter : IDisposable
|
||||||
|
{
|
||||||
|
private readonly FileStream _fs;
|
||||||
|
private readonly int _w, _h, _fps;
|
||||||
|
private readonly List<(long Offset, int Size)> _index = new();
|
||||||
|
private long _riffSizePos, _moviSizePos, _moviDataStart;
|
||||||
|
private long _avihFramesPos, _strhLengthPos, _avihMaxBytesPos, _avihBufPos, _strhBufPos;
|
||||||
|
private int _frames;
|
||||||
|
private long _maxBytes;
|
||||||
|
|
||||||
|
public AviWriter(string path, int width, int height, int fps)
|
||||||
|
{
|
||||||
|
_w = width; _h = height; _fps = fps;
|
||||||
|
_fs = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
WriteHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void W(string s) => _fs.Write(Encoding.ASCII.GetBytes(s));
|
||||||
|
private void W(int v) => _fs.Write(BitConverter.GetBytes(v));
|
||||||
|
private void W(uint v) => _fs.Write(BitConverter.GetBytes(v));
|
||||||
|
private void W(short v) => _fs.Write(BitConverter.GetBytes(v));
|
||||||
|
|
||||||
|
private void WriteHeader()
|
||||||
|
{
|
||||||
|
W("RIFF");
|
||||||
|
_riffSizePos = _fs.Position; W(0);
|
||||||
|
W("AVI ");
|
||||||
|
|
||||||
|
// ---- LIST hdrl ----(size = 'hdrl' + avih块 + strl整块)
|
||||||
|
const int strlContent = 4 + (8 + 56) + (8 + 40); // 'strl' + strh + strf = 116
|
||||||
|
const int strlList = 8 + strlContent; // 124
|
||||||
|
const int hdrlContent = 4 + (8 + 56) + strlList; // 192
|
||||||
|
W("LIST");
|
||||||
|
W(hdrlContent);
|
||||||
|
W("hdrl");
|
||||||
|
|
||||||
|
// avih
|
||||||
|
W("avih"); W(56);
|
||||||
|
long avihStart = _fs.Position;
|
||||||
|
W(1_000_000 / _fps); // dwMicroSecPerFrame
|
||||||
|
_avihMaxBytesPos = _fs.Position; W(0); // dwMaxBytesPerSec
|
||||||
|
W(0); // dwPaddingGranularity
|
||||||
|
W(0x10); // dwFlags: AVIF_HASINDEX
|
||||||
|
_avihFramesPos = _fs.Position; W(0); // dwTotalFrames
|
||||||
|
W(0); // dwInitialFrames
|
||||||
|
W(1); // dwStreams
|
||||||
|
_avihBufPos = _fs.Position; W(0); // dwSuggestedBufferSize
|
||||||
|
W(_w); W(_h);
|
||||||
|
W(0); W(0); W(0); W(0); // reserved
|
||||||
|
|
||||||
|
// ---- LIST strl ----
|
||||||
|
W("LIST");
|
||||||
|
W(strlContent);
|
||||||
|
W("strl");
|
||||||
|
|
||||||
|
W("strh"); W(56);
|
||||||
|
W("vids"); W("MJPG");
|
||||||
|
W(0); // dwFlags
|
||||||
|
W((short)0); // wPriority
|
||||||
|
W((short)0); // wLanguage
|
||||||
|
W(0); // dwInitialFrames
|
||||||
|
W(1); // dwScale
|
||||||
|
W(_fps); // dwRate
|
||||||
|
W(0); // dwStart
|
||||||
|
_strhLengthPos = _fs.Position; W(0); // dwLength
|
||||||
|
_strhBufPos = _fs.Position; W(0); // dwSuggestedBufferSize
|
||||||
|
W(-1); // dwQuality
|
||||||
|
W(0); // dwSampleSize
|
||||||
|
W((short)0); W((short)0); W((short)_w); W((short)_h); // rcFrame
|
||||||
|
|
||||||
|
W("strf"); W(40);
|
||||||
|
W(40); // biSize
|
||||||
|
W(_w); W(_h);
|
||||||
|
W((short)1); // biPlanes
|
||||||
|
W((short)24); // biBitCount
|
||||||
|
W("MJPG"); // biCompression
|
||||||
|
W(_w * _h * 3); // biSizeImage
|
||||||
|
W(0); W(0); W(0); W(0);
|
||||||
|
|
||||||
|
// ---- LIST movi ----
|
||||||
|
W("LIST");
|
||||||
|
_moviSizePos = _fs.Position; W(0);
|
||||||
|
W("movi");
|
||||||
|
_moviDataStart = _fs.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFrame(byte[] jpeg)
|
||||||
|
{
|
||||||
|
long chunkStart = _fs.Position;
|
||||||
|
W("00dc");
|
||||||
|
W(jpeg.Length);
|
||||||
|
_fs.Write(jpeg);
|
||||||
|
if ((jpeg.Length & 1) == 1) _fs.WriteByte(0);
|
||||||
|
|
||||||
|
_index.Add((chunkStart - _moviDataStart, jpeg.Length));
|
||||||
|
_frames++;
|
||||||
|
if (jpeg.Length > _maxBytes) _maxBytes = jpeg.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
long moviEnd = _fs.Position;
|
||||||
|
long moviSize = moviEnd - _moviSizePos - 4; // 不含 'movi' 四字符
|
||||||
|
|
||||||
|
// idx1
|
||||||
|
W("idx1");
|
||||||
|
W(_index.Count * 16);
|
||||||
|
foreach (var (off, size) in _index)
|
||||||
|
{
|
||||||
|
W("00dc");
|
||||||
|
W((uint)0x10);
|
||||||
|
W((int)off);
|
||||||
|
W(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
long fileEnd = _fs.Position;
|
||||||
|
|
||||||
|
// 回填各长度字段
|
||||||
|
void Patch(long pos, int val)
|
||||||
|
{
|
||||||
|
long save = _fs.Position;
|
||||||
|
_fs.Position = pos;
|
||||||
|
_fs.Write(BitConverter.GetBytes(val));
|
||||||
|
_fs.Position = save;
|
||||||
|
}
|
||||||
|
|
||||||
|
Patch(_riffSizePos, (int)(fileEnd - 8));
|
||||||
|
Patch(_moviSizePos, (int)moviSize);
|
||||||
|
Patch(_avihFramesPos, _frames);
|
||||||
|
Patch(_strhLengthPos, _frames);
|
||||||
|
Patch(_avihMaxBytesPos, (int)(_maxBytes * _fps));
|
||||||
|
Patch(_avihBufPos, (int)_maxBytes);
|
||||||
|
Patch(_strhBufPos, (int)_maxBytes);
|
||||||
|
|
||||||
|
_fs.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _fs.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace anjianjingling.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 线程安全日志总线:后台线程产生日志,UI 线程定时消费。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LogBus
|
||||||
|
{
|
||||||
|
private readonly Queue<string> _queue = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
public event Action<string>? OnLog;
|
||||||
|
|
||||||
|
public void Write(string message)
|
||||||
|
{
|
||||||
|
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_queue.Enqueue(line);
|
||||||
|
if (_queue.Count > 2000)
|
||||||
|
{
|
||||||
|
// 防止异常情况下无限增长
|
||||||
|
while (_queue.Count > 1000) _queue.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnLog?.Invoke(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>取出当前所有缓冲日志(UI 定时器调用)。</summary>
|
||||||
|
public List<string> Drain()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var list = new List<string>(_queue.Count);
|
||||||
|
while (_queue.Count > 0) list.Add(_queue.Dequeue());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
namespace anjianjingling.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 键名 → 虚拟键码(VK)映射。
|
||||||
|
/// </summary>
|
||||||
|
public static class VirtualKeys
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, ushort> Map = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["ENTER"] = 0x0D, ["RETURN"] = 0x0D, ["ESC"] = 0x1B, ["ESCAPE"] = 0x1B,
|
||||||
|
["SPACE"] = 0x20, ["TAB"] = 0x09, ["BACKSPACE"] = 0x08, ["BACK"] = 0x08,
|
||||||
|
["DELETE"] = 0x2E, ["DEL"] = 0x2E, ["INSERT"] = 0x2D, ["INS"] = 0x2D,
|
||||||
|
["HOME"] = 0x24, ["END"] = 0x23, ["PAGEUP"] = 0x21, ["PGUP"] = 0x21,
|
||||||
|
["PAGEDOWN"] = 0x22, ["PGDN"] = 0x22,
|
||||||
|
["LEFT"] = 0x25, ["UP"] = 0x26, ["RIGHT"] = 0x27, ["DOWN"] = 0x28,
|
||||||
|
["CAPSLOCK"] = 0x14, ["NUMLOCK"] = 0x90, ["SCROLLLOCK"] = 0x91,
|
||||||
|
["CTRL"] = 0x11, ["CONTROL"] = 0x11, ["ALT"] = 0x12, ["MENU"] = 0x12,
|
||||||
|
["SHIFT"] = 0x10, ["WIN"] = 0x5B, ["LWIN"] = 0x5B,
|
||||||
|
["F1"] = 0x70, ["F2"] = 0x71, ["F3"] = 0x72, ["F4"] = 0x73,
|
||||||
|
["F5"] = 0x74, ["F6"] = 0x75, ["F7"] = 0x76, ["F8"] = 0x77,
|
||||||
|
["F9"] = 0x78, ["F10"] = 0x79, ["F11"] = 0x7A, ["F12"] = 0x7B,
|
||||||
|
["NUMPAD0"] = 0x60, ["NUMPAD1"] = 0x61, ["NUMPAD2"] = 0x62,
|
||||||
|
["NUMPAD3"] = 0x63, ["NUMPAD4"] = 0x64, ["NUMPAD5"] = 0x65,
|
||||||
|
["NUMPAD6"] = 0x66, ["NUMPAD7"] = 0x67, ["NUMPAD8"] = 0x68,
|
||||||
|
["NUMPAD9"] = 0x69,
|
||||||
|
["MULTIPLY"] = 0x6A, ["ADD"] = 0x6B, ["SUBTRACT"] = 0x6D,
|
||||||
|
["DECIMAL"] = 0x6E, ["DIVIDE"] = 0x6F,
|
||||||
|
["OEM_1"] = 0xBA, ["SEMICOLON"] = 0xBA, // ;
|
||||||
|
["OEM_PLUS"] = 0xBB, ["EQUAL"] = 0xBB, // =
|
||||||
|
["OEM_COMMA"] = 0xBC, ["COMMA"] = 0xBC, // ,
|
||||||
|
["OEM_MINUS"] = 0xBD, ["MINUS"] = 0xBD, // -
|
||||||
|
["OEM_PERIOD"] = 0xBE, ["PERIOD"] = 0xBE, // .
|
||||||
|
["OEM_2"] = 0xBF, ["SLASH"] = 0xBF, // /
|
||||||
|
["OEM_3"] = 0xC0, ["BACKQUOTE"] = 0xC0, // `
|
||||||
|
["OEM_4"] = 0xDB, ["LBRACKET"] = 0xDB, // [
|
||||||
|
["OEM_5"] = 0xDC, ["BACKSLASH"] = 0xDC, // \
|
||||||
|
["OEM_6"] = 0xDD, ["RBRACKET"] = 0xDD, // ]
|
||||||
|
["OEM_7"] = 0xDE, ["QUOTE"] = 0xDE, // '
|
||||||
|
};
|
||||||
|
|
||||||
|
static VirtualKeys()
|
||||||
|
{
|
||||||
|
for (char c = 'A'; c <= 'Z'; c++) Map[c.ToString()] = (ushort)c;
|
||||||
|
for (char c = '0'; c <= '9'; c++) Map[c.ToString()] = (ushort)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>尝试解析键名,返回 VK 码;单字符直接按字符 VK 处理。</summary>
|
||||||
|
public static ushort? Resolve(string name)
|
||||||
|
{
|
||||||
|
name = name.Trim();
|
||||||
|
if (name.Length == 0) return null;
|
||||||
|
if (Map.TryGetValue(name, out var vk)) return vk;
|
||||||
|
if (name.Length == 1)
|
||||||
|
{
|
||||||
|
char c = char.ToUpperInvariant(name[0]);
|
||||||
|
if (c >= 'A' && c <= 'Z') return (ushort)c;
|
||||||
|
if (c >= '0' && c <= '9') return (ushort)c;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>是否为扩展键(方向键、Insert/Delete 等)。</summary>
|
||||||
|
public static bool IsExtended(ushort vk)
|
||||||
|
=> vk is >= 0x21 and <= 0x28 or 0x2D or 0x2E or 0x5B or 0x5C or 0x6F or 0x90;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,772 @@
|
|||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using anjianjingling.Lua;
|
||||||
|
|
||||||
|
namespace anjianjingling.Forms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 纯 WinForms 自绘积木画布:插片耦合(榫卯)造型。
|
||||||
|
/// 顶部凹槽 + 底部凸榫互相咬合;容器块为 C 形插槽;参数显示为可插入的白色插槽。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlockCanvas : Panel
|
||||||
|
{
|
||||||
|
private enum RowKind { Block, Slot, Else, End }
|
||||||
|
|
||||||
|
private sealed class Row
|
||||||
|
{
|
||||||
|
public LuaBlock Block = null!;
|
||||||
|
public int Depth;
|
||||||
|
public Rectangle Bounds;
|
||||||
|
public List<LuaBlock> Owner = null!;
|
||||||
|
public int Index;
|
||||||
|
public RowKind Kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 几何(像素)----
|
||||||
|
private const int RowH = 30; // 语句块高度
|
||||||
|
private const int NotchH = 7; // 榫高:凸榫伸出 / 凹槽凹进
|
||||||
|
private const int NotchX = 26; // 榫相对块左边的起始位置
|
||||||
|
private const int NotchSlope = 7; // 斜边宽
|
||||||
|
private const int NotchFlat = 12; // 平台宽
|
||||||
|
private const int WallW = 22; // 容器左壁宽(子块缩进)
|
||||||
|
private const int FootH = 16; // 容器底部封口高
|
||||||
|
private const int ElseH = 22; // 否则分隔条高
|
||||||
|
private const int Radius = 4;
|
||||||
|
private const int MinInner = 24; // 空容器最小内腔高
|
||||||
|
private const int PadX = 12;
|
||||||
|
private const int PadTop = 12;
|
||||||
|
|
||||||
|
private readonly List<Row> _rows = new();
|
||||||
|
private LuaBlock? _selected;
|
||||||
|
private LuaBlock? _hover;
|
||||||
|
private Point _mouseDown;
|
||||||
|
private LuaBlock? _dragBlock;
|
||||||
|
private bool _dragging;
|
||||||
|
private (List<LuaBlock> List, int Index)? _dropTarget;
|
||||||
|
private int _dropY;
|
||||||
|
private int _dropX;
|
||||||
|
|
||||||
|
private static readonly Font TitleFont = new("Microsoft YaHei UI", 9.5F);
|
||||||
|
private static readonly Font SmallFont = new("Microsoft YaHei UI", 7.5F);
|
||||||
|
|
||||||
|
public List<LuaBlock> Blocks { get; } = new();
|
||||||
|
public event Action? Changed;
|
||||||
|
public event Action<LuaBlock?>? SelectionChanged;
|
||||||
|
public event Action<LuaBlock>? EditRequested;
|
||||||
|
|
||||||
|
public LuaBlock? SelectedBlock => _selected;
|
||||||
|
|
||||||
|
public BlockCanvas()
|
||||||
|
{
|
||||||
|
DoubleBuffered = true;
|
||||||
|
BackColor = Color.FromArgb(244, 247, 251);
|
||||||
|
AutoScroll = true;
|
||||||
|
AllowDrop = true;
|
||||||
|
SetStyle(ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer |
|
||||||
|
ControlStyles.AllPaintingInWmPaint | ControlStyles.UserMouse, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 拖入 =====================
|
||||||
|
|
||||||
|
public const string DragFormat = "aj_block_key";
|
||||||
|
|
||||||
|
protected override void OnDragEnter(DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data?.GetDataPresent(DragFormat) == true) e.Effect = DragDropEffects.Copy;
|
||||||
|
base.OnDragEnter(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragOver(DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data?.GetDataPresent(DragFormat) == true)
|
||||||
|
{
|
||||||
|
e.Effect = DragDropEffects.Copy;
|
||||||
|
UpdateDropIndicator(PointToClient(new Point(e.X, e.Y)));
|
||||||
|
}
|
||||||
|
base.OnDragOver(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragLeave(EventArgs e)
|
||||||
|
{
|
||||||
|
_dropTarget = null;
|
||||||
|
Invalidate();
|
||||||
|
base.OnDragLeave(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragDrop(DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data?.GetData(DragFormat) is string key && BlockCatalog.Find(key) != null)
|
||||||
|
{
|
||||||
|
UpdateDropIndicator(PointToClient(new Point(e.X, e.Y)));
|
||||||
|
var block = new LuaBlock { Key = key };
|
||||||
|
InsertAtDropTarget(block);
|
||||||
|
Select(block);
|
||||||
|
}
|
||||||
|
_dropTarget = null;
|
||||||
|
Invalidate();
|
||||||
|
base.OnDragDrop(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 数据操作 =====================
|
||||||
|
|
||||||
|
public void AddBlock(LuaBlock block, bool atEnd = true)
|
||||||
|
{
|
||||||
|
if (atEnd || _selected == null) Blocks.Add(block);
|
||||||
|
else InsertAfter(_selected, block);
|
||||||
|
Select(block);
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertAfter(LuaBlock anchor, LuaBlock block)
|
||||||
|
{
|
||||||
|
foreach (var (list, _) in EnumerateLists(Blocks))
|
||||||
|
{
|
||||||
|
int i = list.IndexOf(anchor);
|
||||||
|
if (i >= 0) { list.Insert(i + 1, block); return; }
|
||||||
|
}
|
||||||
|
Blocks.Add(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(List<LuaBlock> List, int Depth)> EnumerateLists(List<LuaBlock> list, int depth = 0)
|
||||||
|
{
|
||||||
|
yield return (list, depth);
|
||||||
|
foreach (var b in list)
|
||||||
|
{
|
||||||
|
foreach (var x in EnumerateLists(b.Children, depth + 1)) yield return x;
|
||||||
|
foreach (var x in EnumerateLists(b.ElseChildren, depth + 1)) yield return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAll()
|
||||||
|
{
|
||||||
|
Blocks.Clear();
|
||||||
|
_selected = null;
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
SelectionChanged?.Invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>内容有变化(含参数修改)时由外部触发重绘与变更通知。</summary>
|
||||||
|
public void NotifyChanged()
|
||||||
|
{
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadBlocks(IEnumerable<LuaBlock> blocks)
|
||||||
|
{
|
||||||
|
Blocks.Clear();
|
||||||
|
Blocks.AddRange(blocks);
|
||||||
|
_selected = null;
|
||||||
|
AutoScrollPosition = Point.Empty; // 载入后回到顶部,避免旧滚动位置把第一块顶出视野
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
SelectionChanged?.Invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Select(LuaBlock? b)
|
||||||
|
{
|
||||||
|
_selected = b;
|
||||||
|
SelectionChanged?.Invoke(b);
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSelected()
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
RemoveFrom(Blocks, _selected);
|
||||||
|
_selected = null;
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
SelectionChanged?.Invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DuplicateSelected()
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var copy = _selected.Clone();
|
||||||
|
InsertAfter(_selected, copy);
|
||||||
|
Select(copy);
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveSelected(int dir)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
foreach (var (list, _) in EnumerateLists(Blocks))
|
||||||
|
{
|
||||||
|
int i = list.IndexOf(_selected);
|
||||||
|
if (i < 0) continue;
|
||||||
|
int j = i + dir;
|
||||||
|
if (j < 0 || j >= list.Count) return;
|
||||||
|
list.RemoveAt(i);
|
||||||
|
list.Insert(j, _selected);
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleElse()
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var def = BlockCatalog.Find(_selected.Key);
|
||||||
|
if (def is not { Shape: BlockShape.If or BlockShape.IfExists }) return;
|
||||||
|
_selected.HasElse = !_selected.HasElse;
|
||||||
|
if (!_selected.HasElse) _selected.ElseChildren.Clear();
|
||||||
|
Invalidate();
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RemoveFrom(List<LuaBlock> list, LuaBlock target)
|
||||||
|
{
|
||||||
|
if (list.Remove(target)) return true;
|
||||||
|
foreach (var b in list)
|
||||||
|
{
|
||||||
|
if (RemoveFrom(b.Children, target)) return true;
|
||||||
|
if (RemoveFrom(b.ElseChildren, target)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsListInside(LuaBlock root, List<LuaBlock> list)
|
||||||
|
{
|
||||||
|
foreach (var (l, _) in EnumerateLists(root.Children))
|
||||||
|
if (ReferenceEquals(l, list)) return true;
|
||||||
|
foreach (var (l, _) in EnumerateLists(root.ElseChildren))
|
||||||
|
if (ReferenceEquals(l, list)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 鼠标 =====================
|
||||||
|
|
||||||
|
protected override void OnMouseDown(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
Focus();
|
||||||
|
var p = ToLogical(e.Location);
|
||||||
|
var row = HitRow(p);
|
||||||
|
if (row == null || row.Kind is RowKind.Slot or RowKind.End)
|
||||||
|
{
|
||||||
|
// 空槽 / 封口行:不进入拖动,仅选中所属容器
|
||||||
|
Select(row?.Block);
|
||||||
|
base.OnMouseDown(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Button == MouseButtons.Right)
|
||||||
|
{
|
||||||
|
Select(row.Block);
|
||||||
|
BuildMenu().Show(this, p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Select(row.Block);
|
||||||
|
_mouseDown = e.Location;
|
||||||
|
_dragBlock = row.Block;
|
||||||
|
_dragging = false;
|
||||||
|
base.OnMouseDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseMove(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
var lp = ToLogical(e.Location);
|
||||||
|
var h = HitRow(lp);
|
||||||
|
var nb = h?.Block;
|
||||||
|
if (!ReferenceEquals(nb, _hover)) { _hover = nb; Cursor = nb == null ? Cursors.Default : Cursors.Hand; Invalidate(); }
|
||||||
|
|
||||||
|
if (_dragBlock != null && e.Button == MouseButtons.Left)
|
||||||
|
{
|
||||||
|
if (!_dragging && (Math.Abs(e.X - _mouseDown.X) > 6 || Math.Abs(e.Y - _mouseDown.Y) > 6))
|
||||||
|
_dragging = true;
|
||||||
|
if (_dragging)
|
||||||
|
{
|
||||||
|
Cursor = Cursors.SizeAll;
|
||||||
|
UpdateDropIndicator(lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseUp(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (_dragging && _dragBlock != null && _dropTarget is { } t)
|
||||||
|
{
|
||||||
|
var moving = _dragBlock;
|
||||||
|
if (!IsListInside(moving, t.List))
|
||||||
|
{
|
||||||
|
int oldIdx = t.List.IndexOf(moving);
|
||||||
|
RemoveFrom(Blocks, moving);
|
||||||
|
int idx = t.Index;
|
||||||
|
if (oldIdx >= 0 && oldIdx < idx) idx--;
|
||||||
|
idx = Math.Clamp(idx, 0, t.List.Count);
|
||||||
|
t.List.Insert(idx, moving);
|
||||||
|
Select(moving);
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_dragBlock = null;
|
||||||
|
_dragging = false;
|
||||||
|
_dropTarget = null;
|
||||||
|
Cursor = Cursors.Default;
|
||||||
|
Invalidate();
|
||||||
|
base.OnMouseUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseDoubleClick(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
var row = HitRow(ToLogical(e.Location));
|
||||||
|
if (row != null && row.Kind == RowKind.Block) EditRequested?.Invoke(row.Block);
|
||||||
|
base.OnMouseDoubleClick(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseLeave(EventArgs e)
|
||||||
|
{
|
||||||
|
if (_hover != null) { _hover = null; Invalidate(); }
|
||||||
|
base.OnMouseLeave(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected != null)
|
||||||
|
{
|
||||||
|
switch (e.KeyCode)
|
||||||
|
{
|
||||||
|
case Keys.Delete: RemoveSelected(); e.Handled = true; break;
|
||||||
|
case Keys.Up when e.Control: MoveSelected(-1); e.Handled = true; break;
|
||||||
|
case Keys.Down when e.Control: MoveSelected(1); e.Handled = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContextMenuStrip BuildMenu()
|
||||||
|
{
|
||||||
|
var m = new ContextMenuStrip { Font = new Font("Microsoft YaHei UI", 9F) };
|
||||||
|
m.Items.Add("编辑参数", null, (s, e) => { if (_selected != null) EditRequested?.Invoke(_selected); });
|
||||||
|
m.Items.Add("复制一份", null, (s, e) => DuplicateSelected());
|
||||||
|
m.Items.Add("上移", null, (s, e) => MoveSelected(-1));
|
||||||
|
m.Items.Add("下移", null, (s, e) => MoveSelected(1));
|
||||||
|
m.Items.Add(new ToolStripSeparator());
|
||||||
|
var def = _selected == null ? null : BlockCatalog.Find(_selected.Key);
|
||||||
|
if (def is { Shape: BlockShape.If or BlockShape.IfExists })
|
||||||
|
m.Items.Add(_selected!.HasElse ? "移除「否则」分支" : "添加「否则」分支", null, (s, e) => ToggleElse());
|
||||||
|
m.Items.Add("删除", null, (s, e) => RemoveSelected());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 命中 / 插入点 =====================
|
||||||
|
|
||||||
|
private Point ToLogical(Point client)
|
||||||
|
=> new(client.X - AutoScrollPosition.X, client.Y - AutoScrollPosition.Y);
|
||||||
|
|
||||||
|
private Row? HitRow(Point p)
|
||||||
|
{
|
||||||
|
// 后画的在上面,倒序命中
|
||||||
|
for (int i = _rows.Count - 1; i >= 0; i--)
|
||||||
|
if (_rows[i].Bounds.Contains(p)) return _rows[i];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDropIndicator(Point p)
|
||||||
|
{
|
||||||
|
var hit = ComputeInsert(p);
|
||||||
|
_dropTarget = (hit.List, hit.Index);
|
||||||
|
_dropY = hit.Y;
|
||||||
|
_dropX = hit.X;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private (List<LuaBlock> List, int Index, int Y, int X) ComputeInsert(Point p)
|
||||||
|
{
|
||||||
|
if (_rows.Count == 0) return (Blocks, 0, PadTop, PadX);
|
||||||
|
|
||||||
|
Row? hit = null;
|
||||||
|
foreach (var r in _rows)
|
||||||
|
if (p.Y >= r.Bounds.Top && p.Y <= r.Bounds.Bottom) { hit = r; break; }
|
||||||
|
if (hit == null)
|
||||||
|
{
|
||||||
|
for (int i = _rows.Count - 1; i >= 0; i--)
|
||||||
|
if (p.Y > _rows[i].Bounds.Bottom) { hit = _rows[i]; break; }
|
||||||
|
}
|
||||||
|
if (hit == null) return (Blocks, 0, PadTop, PadX);
|
||||||
|
|
||||||
|
int x = hit.Bounds.Left;
|
||||||
|
bool upper = p.Y < hit.Bounds.Top + hit.Bounds.Height / 2;
|
||||||
|
|
||||||
|
switch (hit.Kind)
|
||||||
|
{
|
||||||
|
case RowKind.Slot:
|
||||||
|
return (hit.Owner, hit.Index, hit.Bounds.Top + 2, x + WallW);
|
||||||
|
case RowKind.Else:
|
||||||
|
return upper ? (hit.Block.Children, hit.Block.Children.Count, hit.Bounds.Top + 1, x)
|
||||||
|
: (hit.Owner, hit.Index, hit.Bounds.Bottom + 1, x);
|
||||||
|
case RowKind.End:
|
||||||
|
return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left + WallW);
|
||||||
|
default:
|
||||||
|
if (upper) return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left);
|
||||||
|
var def = BlockCatalog.Find(hit.Block.Key);
|
||||||
|
if (def?.IsContainer == true)
|
||||||
|
return (hit.Block.Children, 0, hit.Bounds.Bottom + 2, hit.Bounds.Left + WallW);
|
||||||
|
return (hit.Owner, hit.Index + 1, hit.Bounds.Bottom + 1, hit.Bounds.Left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertAtDropTarget(LuaBlock block)
|
||||||
|
{
|
||||||
|
if (_dropTarget is { } t)
|
||||||
|
{
|
||||||
|
int idx = Math.Clamp(t.Index, 0, t.List.Count);
|
||||||
|
t.List.Insert(idx, block);
|
||||||
|
}
|
||||||
|
else Blocks.Add(block);
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 绘制 =====================
|
||||||
|
|
||||||
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPaint(e);
|
||||||
|
var g = e.Graphics;
|
||||||
|
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
|
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
|
||||||
|
g.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y);
|
||||||
|
|
||||||
|
_rows.Clear();
|
||||||
|
int y = PadTop;
|
||||||
|
int w = Math.Max(200, ClientSize.Width - PadX - 20);
|
||||||
|
DrawList(g, Blocks, 0, ref y, PadX, w);
|
||||||
|
|
||||||
|
if (Blocks.Count == 0)
|
||||||
|
{
|
||||||
|
using var f = new Font("Microsoft YaHei UI", 10F);
|
||||||
|
using var b = new SolidBrush(Color.FromArgb(150, 160, 178));
|
||||||
|
g.DrawString("从左侧命令库双击或拖入积木,开始搭建流程", f, b, PadX + 8, PadTop + 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dropTarget != null)
|
||||||
|
{
|
||||||
|
using var pen = new Pen(Color.FromArgb(37, 99, 235), 2);
|
||||||
|
int ww = Math.Max(140, ClientSize.Width - _dropX - 30);
|
||||||
|
g.DrawLine(pen, _dropX, _dropY, _dropX + ww, _dropY);
|
||||||
|
g.FillEllipse(Brushes.DodgerBlue, _dropX - 3, _dropY - 3, 6, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalH = y + NotchH + 30;
|
||||||
|
if (AutoScrollMinSize.Height != totalH)
|
||||||
|
AutoScrollMinSize = new Size(0, totalH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MeasureList(List<LuaBlock> list)
|
||||||
|
{
|
||||||
|
int h = 0;
|
||||||
|
foreach (var b in list)
|
||||||
|
{
|
||||||
|
var def = BlockCatalog.Find(b.Key);
|
||||||
|
if (def == null) continue;
|
||||||
|
h += RowH;
|
||||||
|
if (def.IsContainer) h += InnerHeight(b, def) + FootH;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int InnerHeight(LuaBlock b, BlockDef def)
|
||||||
|
{
|
||||||
|
int h = MeasureList(b.Children);
|
||||||
|
if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse)
|
||||||
|
h += ElseH + MeasureList(b.ElseChildren);
|
||||||
|
return Math.Max(MinInner, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawList(Graphics g, List<LuaBlock> list, int depth, ref int y, int x, int w)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
var b = list[i];
|
||||||
|
var def = BlockCatalog.Find(b.Key);
|
||||||
|
if (def == null) continue;
|
||||||
|
|
||||||
|
bool sel = ReferenceEquals(b, _selected);
|
||||||
|
bool dim = _dragging && ReferenceEquals(b, _dragBlock);
|
||||||
|
var color = CategoryColor(def.Category);
|
||||||
|
|
||||||
|
if (!def.IsContainer)
|
||||||
|
{
|
||||||
|
var rect = new Rectangle(x, y, w, RowH);
|
||||||
|
using (var path = PuzzlePath(x, y, w, RowH, NotchX))
|
||||||
|
{
|
||||||
|
FillBlock(g, path, rect, color, sel, dim);
|
||||||
|
}
|
||||||
|
DrawLabel(g, b, def, x + 12, y + 7, w - 24, color);
|
||||||
|
_rows.Add(new Row { Block = b, Depth = depth, Bounds = rect, Owner = list, Index = i, Kind = RowKind.Block });
|
||||||
|
y += RowH;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 容器块(C 形插槽)----------
|
||||||
|
int innerH = InnerHeight(b, def);
|
||||||
|
int headTop = y;
|
||||||
|
int innerTop = headTop + RowH;
|
||||||
|
int footTop = innerTop + innerH;
|
||||||
|
int footBottom = footTop + FootH;
|
||||||
|
|
||||||
|
using (var shell = ContainerPath(x, headTop, w, RowH, innerH, FootH, NotchX))
|
||||||
|
{
|
||||||
|
FillBlock(g, shell, new Rectangle(x, headTop, w, footBottom - headTop), color, sel, dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内腔插槽底
|
||||||
|
using (var innerBg = new SolidBrush(Color.FromArgb(233, 238, 246)))
|
||||||
|
g.FillRectangle(innerBg, x + WallW, innerTop, w - WallW, innerH);
|
||||||
|
using (var lipPen = new Pen(Darken(color, 0.18), 1f))
|
||||||
|
g.DrawRectangle(lipPen, x + WallW, innerTop, w - WallW - 1, innerH - 1);
|
||||||
|
|
||||||
|
DrawLabel(g, b, def, x + 12, headTop + 7, w - 24, color);
|
||||||
|
_rows.Add(new Row
|
||||||
|
{
|
||||||
|
Block = b, Depth = depth,
|
||||||
|
Bounds = new Rectangle(x, headTop, w, RowH),
|
||||||
|
Owner = list, Index = i, Kind = RowKind.Block
|
||||||
|
});
|
||||||
|
|
||||||
|
int cy = innerTop;
|
||||||
|
int iw = w - WallW;
|
||||||
|
if (b.Children.Count == 0)
|
||||||
|
{
|
||||||
|
_rows.Add(new Row
|
||||||
|
{
|
||||||
|
Block = b, Depth = depth + 1,
|
||||||
|
Bounds = new Rectangle(x + WallW, cy, iw, Math.Max(20, innerH - NotchH)),
|
||||||
|
Owner = b.Children, Index = 0, Kind = RowKind.Slot
|
||||||
|
});
|
||||||
|
DrawSlotHint(g, x + WallW + 8, cy + 6);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawList(g, b.Children, depth + 1, ref cy, x + WallW, iw);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tailList = b.Children;
|
||||||
|
if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse)
|
||||||
|
{
|
||||||
|
int elseY = innerTop + NotchH + Math.Max(0, MeasureList(b.Children));
|
||||||
|
using var eb = new SolidBrush(Darken(color, 0.18));
|
||||||
|
g.FillRectangle(eb, x, elseY, w, ElseH);
|
||||||
|
using var ef = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
|
||||||
|
using var et = new SolidBrush(Color.FromArgb(240, 245, 252));
|
||||||
|
g.DrawString("否则", ef, et, x + WallW + 4, elseY + 3);
|
||||||
|
_rows.Add(new Row
|
||||||
|
{
|
||||||
|
Block = b, Depth = depth + 1,
|
||||||
|
Bounds = new Rectangle(x + WallW, elseY, iw, ElseH),
|
||||||
|
Owner = b.ElseChildren, Index = 0, Kind = RowKind.Else
|
||||||
|
});
|
||||||
|
|
||||||
|
int ey = elseY + ElseH;
|
||||||
|
if (b.ElseChildren.Count == 0)
|
||||||
|
{
|
||||||
|
_rows.Add(new Row
|
||||||
|
{
|
||||||
|
Block = b, Depth = depth + 1,
|
||||||
|
Bounds = new Rectangle(x + WallW, ey, iw, Math.Max(18, footTop - ey)),
|
||||||
|
Owner = b.ElseChildren, Index = 0, Kind = RowKind.Slot
|
||||||
|
});
|
||||||
|
DrawSlotHint(g, x + WallW + 8, ey + 4);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawList(g, b.ElseChildren, depth + 1, ref ey, x + WallW, iw);
|
||||||
|
}
|
||||||
|
tailList = b.ElseChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rows.Add(new Row
|
||||||
|
{
|
||||||
|
Block = b, Depth = depth,
|
||||||
|
Bounds = new Rectangle(x, footTop, w, FootH),
|
||||||
|
Owner = tailList, Index = tailList.Count, Kind = RowKind.End
|
||||||
|
});
|
||||||
|
y = footBottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FillBlock(Graphics g, GraphicsPath path, Rectangle r, Color color, bool sel, bool dim)
|
||||||
|
{
|
||||||
|
using (var fill = new LinearGradientBrush(
|
||||||
|
new Point(r.Left, r.Top), new Point(r.Left, r.Bottom),
|
||||||
|
Lighten(color, dim ? 0.42 : 0.16), dim ? Lighten(color, 0.42) : color))
|
||||||
|
{
|
||||||
|
g.FillPath(fill, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var edge = new Pen(Darken(color, 0.32), 1f);
|
||||||
|
g.DrawPath(edge, path);
|
||||||
|
|
||||||
|
if (sel)
|
||||||
|
{
|
||||||
|
using var glow = new Pen(Color.FromArgb(250, 204, 21), 2.5f);
|
||||||
|
g.DrawPath(glow, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawSlotHint(Graphics g, int x, int y)
|
||||||
|
{
|
||||||
|
using var f = new Font("Microsoft YaHei UI", 8F);
|
||||||
|
using var b = new SolidBrush(Color.FromArgb(158, 170, 190));
|
||||||
|
g.DrawString("把积木拖到这里", f, b, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 标题(参数画成白色插槽)----------
|
||||||
|
|
||||||
|
private void DrawLabel(Graphics g, LuaBlock b, BlockDef def, int x, int y, int maxW, Color color)
|
||||||
|
{
|
||||||
|
float cx = x;
|
||||||
|
using var white = new SolidBrush(Color.White);
|
||||||
|
using var slotBg = new SolidBrush(Color.FromArgb(246, 248, 252));
|
||||||
|
using var slotFg = new SolidBrush(Color.FromArgb(48, 58, 74));
|
||||||
|
using var fmtC = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
|
||||||
|
|
||||||
|
foreach (var (text, isArg) in SplitTitle(def, b))
|
||||||
|
{
|
||||||
|
if (text.Length == 0) continue;
|
||||||
|
if (cx > x + maxW) break;
|
||||||
|
|
||||||
|
if (!isArg)
|
||||||
|
{
|
||||||
|
g.DrawString(text, TitleFont, white, cx, y);
|
||||||
|
cx += g.MeasureString(text, TitleFont).Width;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sz = g.MeasureString(text, TitleFont);
|
||||||
|
float wpx = Math.Min(sz.Width + 14, Math.Max(48, maxW / 3));
|
||||||
|
var rect = new RectangleF(cx, y - 2f, wpx, 20);
|
||||||
|
using var path = Rounded(Rectangle.Round(rect), 5);
|
||||||
|
g.FillPath(slotBg, path);
|
||||||
|
using var pen = new Pen(Darken(color, 0.30), 1f);
|
||||||
|
g.DrawPath(pen, path);
|
||||||
|
var clip = new RectangleF(rect.Left + 2, rect.Top, rect.Width - 4, rect.Height);
|
||||||
|
g.DrawString(text, TitleFont, slotFg, clip, fmtC);
|
||||||
|
cx += wpx + 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<(string Text, bool IsArg)> SplitTitle(BlockDef def, LuaBlock b)
|
||||||
|
{
|
||||||
|
var segs = new List<(string, bool)>();
|
||||||
|
if (def.Args.Length == 0) { segs.Add((def.Title, false)); return segs; }
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
for (int i = 0; i < def.Args.Length; i++)
|
||||||
|
{
|
||||||
|
string token = "{" + i + "}";
|
||||||
|
int idx = def.Title.IndexOf(token, StringComparison.Ordinal);
|
||||||
|
if (idx < 0 || idx < pos) continue;
|
||||||
|
if (idx > pos) segs.Add((def.Title[pos..idx], false));
|
||||||
|
segs.Add((b.Value(i), true));
|
||||||
|
pos = idx + token.Length;
|
||||||
|
}
|
||||||
|
if (pos < def.Title.Length) segs.Add((def.Title[pos..], false));
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 路径:插片耦合造型 ----------
|
||||||
|
|
||||||
|
private static void TopEdge(GraphicsPath p, int x, int y, int w, int r, int nx)
|
||||||
|
{
|
||||||
|
p.AddLine(x + r, y, x + nx, y);
|
||||||
|
p.AddLine(x + nx, y, x + nx + NotchSlope, y + NotchH);
|
||||||
|
p.AddLine(x + nx + NotchSlope, y + NotchH, x + nx + NotchSlope + NotchFlat, y + NotchH);
|
||||||
|
p.AddLine(x + nx + NotchSlope + NotchFlat, y + NotchH, x + nx + 2 * NotchSlope + NotchFlat, y);
|
||||||
|
p.AddLine(x + nx + 2 * NotchSlope + NotchFlat, y, x + w - r, y);
|
||||||
|
p.AddArc(x + w - 2 * r, y, 2 * r, 2 * r, 270, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BottomEdge(GraphicsPath p, int x, int bottom, int w, int r, int nx)
|
||||||
|
{
|
||||||
|
int nEnd = x + nx + 2 * NotchSlope + NotchFlat;
|
||||||
|
p.AddLine(x + w - r, bottom, nEnd, bottom);
|
||||||
|
p.AddLine(nEnd, bottom, nEnd - NotchSlope, bottom + NotchH);
|
||||||
|
p.AddLine(nEnd - NotchSlope, bottom + NotchH, nEnd - NotchSlope - NotchFlat, bottom + NotchH);
|
||||||
|
p.AddLine(nEnd - NotchSlope - NotchFlat, bottom + NotchH, x + nx, bottom);
|
||||||
|
p.AddLine(x + nx, bottom, x + r, bottom);
|
||||||
|
p.AddArc(x, bottom - 2 * r, 2 * r, 2 * r, 90, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>单块:顶部凹槽 + 底部凸榫。</summary>
|
||||||
|
private static GraphicsPath PuzzlePath(int x, int y, int w, int h, int nx)
|
||||||
|
{
|
||||||
|
var p = new GraphicsPath();
|
||||||
|
int d = Radius * 2;
|
||||||
|
int bottom = y + h;
|
||||||
|
p.StartFigure();
|
||||||
|
p.AddArc(x, y, d, d, 180, 90); // 左上
|
||||||
|
TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上
|
||||||
|
p.AddLine(x + w, y + Radius, x + w, bottom - Radius); // 右
|
||||||
|
p.AddArc(x + w - d, bottom - d, d, d, 0, 90); // 右下
|
||||||
|
BottomEdge(p, x, bottom, w, Radius, nx); // 底边(凸榫)+ 左下
|
||||||
|
p.CloseFigure();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>容器块:C 形,中间挖出插槽。</summary>
|
||||||
|
private static GraphicsPath ContainerPath(int x, int y, int w, int headH, int innerH, int footH, int nx)
|
||||||
|
{
|
||||||
|
var p = new GraphicsPath();
|
||||||
|
int d = Radius * 2;
|
||||||
|
int innerTop = y + headH;
|
||||||
|
int footTop = innerTop + innerH;
|
||||||
|
int footBottom = footTop + footH;
|
||||||
|
|
||||||
|
p.StartFigure();
|
||||||
|
p.AddArc(x, y, d, d, 180, 90); // 左上
|
||||||
|
TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上
|
||||||
|
p.AddLine(x + w, y + Radius, x + w, footBottom - Radius); // 右外墙
|
||||||
|
p.AddArc(x + w - d, footBottom - d, d, d, 0, 90); // 右下
|
||||||
|
BottomEdge(p, x, footBottom, w, Radius, nx); // 底边(凸榫)+ 左下
|
||||||
|
p.AddLine(x, footBottom - Radius, x, footTop + Radius); // 左外墙上返
|
||||||
|
p.AddArc(x, footTop, d, d, 90, 90); // 封口左上内角
|
||||||
|
p.AddLine(x + Radius, footTop, x + WallW, footTop); // 插槽底边
|
||||||
|
p.AddLine(x + WallW, footTop, x + WallW, innerTop); // 左壁内侧
|
||||||
|
p.AddLine(x + WallW, innerTop, x + Radius, innerTop); // 插槽顶边
|
||||||
|
p.AddArc(x, innerTop - d, d, d, 270, 90); // 插槽左上内角
|
||||||
|
p.CloseFigure();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphicsPath Rounded(Rectangle r, int radius)
|
||||||
|
{
|
||||||
|
var p = new GraphicsPath();
|
||||||
|
int d = radius * 2;
|
||||||
|
p.AddArc(r.Left, r.Top, d, d, 180, 90);
|
||||||
|
p.AddArc(r.Right - d, r.Top, d, d, 270, 90);
|
||||||
|
p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
|
||||||
|
p.AddArc(r.Left, r.Bottom - d, d, d, 90, 90);
|
||||||
|
p.CloseFigure();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 颜色 ----------
|
||||||
|
|
||||||
|
private static Color CategoryColor(string category) => category switch
|
||||||
|
{
|
||||||
|
"流程" => Color.FromArgb(59, 130, 246),
|
||||||
|
"鼠标" => Color.FromArgb(16, 175, 122),
|
||||||
|
"键盘" => Color.FromArgb(214, 140, 18),
|
||||||
|
"视觉" => Color.FromArgb(139, 92, 246),
|
||||||
|
"系统" => Color.FromArgb(226, 104, 72),
|
||||||
|
"浏览器" => Color.FromArgb(14, 160, 226),
|
||||||
|
_ => Color.FromArgb(100, 116, 139)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color Lighten(Color c, double f) => Color.FromArgb(
|
||||||
|
(int)Math.Min(255, c.R + 255 * f),
|
||||||
|
(int)Math.Min(255, c.G + 255 * f),
|
||||||
|
(int)Math.Min(255, c.B + 255 * f));
|
||||||
|
|
||||||
|
private static Color Darken(Color c, double f) => Color.FromArgb(
|
||||||
|
(int)(c.R * (1 - f)), (int)(c.G * (1 - f)), (int)(c.B * (1 - f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using anjianjingling.Native;
|
||||||
|
|
||||||
|
namespace anjianjingling.Forms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lua 脚本编辑器:行号栏 + 语法着色(关键字/字符串/注释/数字)。
|
||||||
|
/// 纯 WinForms,无第三方依赖。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LuaEditor : UserControl
|
||||||
|
{
|
||||||
|
private readonly RichTextBox _rtb = new();
|
||||||
|
private readonly Panel _gutter = new();
|
||||||
|
private readonly System.Windows.Forms.Timer _hlTimer = new();
|
||||||
|
private bool _highlighting;
|
||||||
|
|
||||||
|
private const int WM_SETREDRAW = 0x000B;
|
||||||
|
private const int WM_USER = 0x0400;
|
||||||
|
private const int EM_GETFIRSTVISIBLELINE = WM_USER + 206;
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
public LuaEditor()
|
||||||
|
{
|
||||||
|
_gutter.Dock = DockStyle.Left;
|
||||||
|
_gutter.Width = 46;
|
||||||
|
_gutter.BackColor = Color.FromArgb(240, 243, 248);
|
||||||
|
_gutter.Paint += GutterPaint;
|
||||||
|
|
||||||
|
_rtb.Dock = DockStyle.Fill;
|
||||||
|
_rtb.BorderStyle = BorderStyle.None;
|
||||||
|
_rtb.Font = new Font("Consolas", 11F);
|
||||||
|
_rtb.BackColor = Color.FromArgb(252, 253, 255);
|
||||||
|
_rtb.ForeColor = Color.FromArgb(30, 34, 42);
|
||||||
|
_rtb.WordWrap = false;
|
||||||
|
_rtb.AcceptsTab = true;
|
||||||
|
_rtb.ScrollBars = RichTextBoxScrollBars.Both;
|
||||||
|
_rtb.DetectUrls = false;
|
||||||
|
_rtb.HideSelection = false;
|
||||||
|
_rtb.VScroll += (s, e) => _gutter.Invalidate();
|
||||||
|
_rtb.HScroll += (s, e) => _gutter.Invalidate();
|
||||||
|
_rtb.TextChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
_hlTimer.Stop();
|
||||||
|
_hlTimer.Start();
|
||||||
|
ScriptChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
_gutter.Invalidate();
|
||||||
|
};
|
||||||
|
_rtb.SelectionChanged += (s, e) => CaretMoved?.Invoke(LineOf(_rtb.SelectionStart), ColumnOf(_rtb.SelectionStart));
|
||||||
|
_rtb.KeyDown += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.Control && e.KeyCode == Keys.A)
|
||||||
|
{
|
||||||
|
_rtb.SelectAll();
|
||||||
|
e.SuppressKeyPress = true;
|
||||||
|
}
|
||||||
|
// Tab 插入 4 空格而不是制表符
|
||||||
|
if (e.KeyCode == Keys.Tab && !e.Shift && !e.Control)
|
||||||
|
{
|
||||||
|
_rtb.SelectedText = " ";
|
||||||
|
e.SuppressKeyPress = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_hlTimer.Interval = 180;
|
||||||
|
_hlTimer.Tick += (s, e) => { _hlTimer.Stop(); Highlight(); };
|
||||||
|
|
||||||
|
Controls.Add(_rtb);
|
||||||
|
Controls.Add(_gutter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>脚本文本(不用基类 Text,避免与控件标题混淆)。</summary>
|
||||||
|
[System.ComponentModel.Browsable(false)]
|
||||||
|
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
|
||||||
|
public string Script
|
||||||
|
{
|
||||||
|
get => _rtb.Text;
|
||||||
|
set { _rtb.Text = value; Highlight(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public RichTextBox Inner => _rtb;
|
||||||
|
public event EventHandler? ScriptChanged;
|
||||||
|
public event Action<int, int>? CaretMoved;
|
||||||
|
|
||||||
|
public void FocusEditor() => _rtb.Focus();
|
||||||
|
|
||||||
|
public int CurrentLine => LineOf(_rtb.SelectionStart);
|
||||||
|
|
||||||
|
public void GotoLine(int line)
|
||||||
|
{
|
||||||
|
var lines = _rtb.Lines;
|
||||||
|
if (line < 0 || line >= lines.Length) return;
|
||||||
|
int idx = 0;
|
||||||
|
for (int i = 0; i < line; i++) idx += lines[i].Length + 1;
|
||||||
|
_rtb.SelectionStart = idx;
|
||||||
|
_rtb.SelectionLength = 0;
|
||||||
|
_rtb.ScrollToCaret();
|
||||||
|
_rtb.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int LineOf(int charIndex)
|
||||||
|
{
|
||||||
|
int line = _rtb.GetLineFromCharIndex(charIndex);
|
||||||
|
return line + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ColumnOf(int charIndex)
|
||||||
|
{
|
||||||
|
int first = _rtb.GetFirstCharIndexFromLine(_rtb.GetLineFromCharIndex(charIndex));
|
||||||
|
return charIndex - first + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 行号 =====================
|
||||||
|
|
||||||
|
private void GutterPaint(object? sender, PaintEventArgs e)
|
||||||
|
{
|
||||||
|
var g = e.Graphics;
|
||||||
|
g.Clear(_gutter.BackColor);
|
||||||
|
using var font = new Font("Consolas", 9.5F);
|
||||||
|
using var brush = new SolidBrush(Color.FromArgb(130, 142, 162));
|
||||||
|
using var curBrush = new SolidBrush(Color.FromArgb(37, 99, 235));
|
||||||
|
|
||||||
|
int firstVisible = (int)SendMessage(_rtb.Handle, EM_GETFIRSTVISIBLELINE, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
int lineCount = _rtb.Lines.Length;
|
||||||
|
int lineHeight = _rtb.Font.Height;
|
||||||
|
|
||||||
|
int curLine = _rtb.GetLineFromCharIndex(_rtb.SelectionStart);
|
||||||
|
for (int i = 0; i < 200; i++)
|
||||||
|
{
|
||||||
|
int lineIdx = firstVisible + i;
|
||||||
|
if (lineIdx >= lineCount) break;
|
||||||
|
int y = i * lineHeight + 2;
|
||||||
|
if (y > _gutter.Height) break;
|
||||||
|
string num = (lineIdx + 1).ToString();
|
||||||
|
var sz = g.MeasureString(num, font);
|
||||||
|
var b = lineIdx == curLine ? curBrush : brush;
|
||||||
|
g.DrawString(num, font, b, _gutter.Width - sz.Width - 8, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 语法着色 =====================
|
||||||
|
|
||||||
|
private static readonly Regex RxComment = new(@"--\[\[.*?\]\]|--[^\n]*", RegexOptions.Compiled | RegexOptions.Singleline);
|
||||||
|
private static readonly Regex RxString = new("\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex RxNumber = new(@"\b0x[0-9a-fA-F]+\b|\b\d+(\.\d+)?\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex RxKeyword = new(
|
||||||
|
@"\b(and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
private static readonly Regex RxApi = new(
|
||||||
|
@"\b(delay|delayRandom|moveTo|moveR|leftClick|rightClick|middleClick|doubleClick|wheel|drag|keyPress|keyDown|keyUp|hotkey|typeText|sayText|findImage|findColor|waitImage|exists|screenshot|colorAt|clipGet|clipSet|run|activate|log|stop|random|now|sleep|mouse|key|screen|win|sys|clip|browser)\b",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Color CKeyword = Color.FromArgb(175, 45, 130);
|
||||||
|
private static readonly Color CString = Color.FromArgb(20, 130, 80);
|
||||||
|
private static readonly Color CComment = Color.FromArgb(140, 150, 165);
|
||||||
|
private static readonly Color CNumber = Color.FromArgb(198, 106, 22);
|
||||||
|
private static readonly Color CApi = Color.FromArgb(30, 90, 200);
|
||||||
|
private static readonly Color CPlain = Color.FromArgb(30, 34, 42);
|
||||||
|
|
||||||
|
public void Highlight()
|
||||||
|
{
|
||||||
|
if (_highlighting || _rtb.TextLength == 0) return;
|
||||||
|
_highlighting = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string text = _rtb.Text;
|
||||||
|
int selStart = _rtb.SelectionStart;
|
||||||
|
int selLen = _rtb.SelectionLength;
|
||||||
|
|
||||||
|
SendMessage(_rtb.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
|
||||||
|
_rtb.SelectAll();
|
||||||
|
_rtb.SelectionColor = CPlain;
|
||||||
|
|
||||||
|
void Paint(Regex rx, Color color)
|
||||||
|
{
|
||||||
|
foreach (Match m in rx.Matches(text))
|
||||||
|
{
|
||||||
|
if (m.Length == 0) continue;
|
||||||
|
_rtb.Select(m.Index, m.Length);
|
||||||
|
_rtb.SelectionColor = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Paint(RxNumber, CNumber);
|
||||||
|
Paint(RxKeyword, CKeyword);
|
||||||
|
Paint(RxApi, CApi);
|
||||||
|
Paint(RxString, CString);
|
||||||
|
Paint(RxComment, CComment);
|
||||||
|
|
||||||
|
_rtb.Select(selStart, selLen);
|
||||||
|
_rtb.SelectionColor = CPlain;
|
||||||
|
|
||||||
|
SendMessage(_rtb.Handle, WM_SETREDRAW, (IntPtr)1, IntPtr.Zero);
|
||||||
|
_rtb.Invalidate();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally { _highlighting = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1995
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,133 @@
|
|||||||
|
using anjianjingling.Native;
|
||||||
|
|
||||||
|
namespace anjianjingling.Forms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 全屏框选截图:鼠标拖拽框选区域,松开后截图保存到 images 目录。
|
||||||
|
/// ESC 取消。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OverlayForm : Form
|
||||||
|
{
|
||||||
|
private Point _start, _end;
|
||||||
|
private bool _dragging;
|
||||||
|
private Rectangle _selected;
|
||||||
|
public string? SavedPath { get; private set; }
|
||||||
|
public event Action<string>? OnSaved;
|
||||||
|
|
||||||
|
/// <summary>是否把框选结果保存为图片(录屏取区域时设为 false)。</summary>
|
||||||
|
[System.ComponentModel.Browsable(false)]
|
||||||
|
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
|
||||||
|
public bool SaveToFile { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>最近一次框选的屏幕矩形。</summary>
|
||||||
|
[System.ComponentModel.Browsable(false)]
|
||||||
|
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
|
||||||
|
public Rectangle SelectedRect { get; private set; }
|
||||||
|
|
||||||
|
public OverlayForm()
|
||||||
|
{
|
||||||
|
FormBorderStyle = FormBorderStyle.None;
|
||||||
|
StartPosition = FormStartPosition.Manual;
|
||||||
|
Bounds = SystemInformation.VirtualScreen;
|
||||||
|
TopMost = true;
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
DoubleBuffered = true;
|
||||||
|
KeyPreview = true;
|
||||||
|
BackColor = Color.Black;
|
||||||
|
Opacity = 0.25;
|
||||||
|
Cursor = Cursors.Cross;
|
||||||
|
|
||||||
|
MouseDown += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.Button == MouseButtons.Left)
|
||||||
|
{
|
||||||
|
_dragging = true;
|
||||||
|
_start = _end = PointToScreen(e.Location);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MouseMove += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_dragging)
|
||||||
|
{
|
||||||
|
_end = PointToScreen(e.Location);
|
||||||
|
var r = RectangleFromPoints(_start, _end);
|
||||||
|
_selected = r;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MouseUp += (s, e) =>
|
||||||
|
{
|
||||||
|
if (_dragging && e.Button == MouseButtons.Left)
|
||||||
|
{
|
||||||
|
_dragging = false;
|
||||||
|
var r = RectangleFromPoints(_start, _end);
|
||||||
|
if (r.Width > 4 && r.Height > 4)
|
||||||
|
{
|
||||||
|
SelectedRect = r;
|
||||||
|
if (SaveToFile)
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(AppContext.BaseDirectory, "images");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
string file = Path.Combine(dir,
|
||||||
|
$"clip_{DateTime.Now:yyyyMMdd_HHmmss}.png");
|
||||||
|
using (var bmp = new Bitmap(r.Width, r.Height))
|
||||||
|
using (var g = Graphics.FromImage(bmp))
|
||||||
|
{
|
||||||
|
g.CopyFromScreen(r.Left, r.Top, 0, 0, r.Size);
|
||||||
|
bmp.Save(file, System.Drawing.Imaging.ImageFormat.Png);
|
||||||
|
}
|
||||||
|
SavedPath = file;
|
||||||
|
OnSaved?.Invoke(file);
|
||||||
|
}
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
KeyDown += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.KeyCode == Keys.Escape)
|
||||||
|
{
|
||||||
|
DialogResult = DialogResult.Cancel;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPaint(e);
|
||||||
|
if (_selected.Width > 0)
|
||||||
|
{
|
||||||
|
// 用屏幕坐标转客户区绘制
|
||||||
|
var r = ScreenRectToClient(_selected);
|
||||||
|
using (var pen = new Pen(Color.Lime, 1))
|
||||||
|
{
|
||||||
|
e.Graphics.DrawRectangle(pen, r);
|
||||||
|
string info = $"{_selected.Width} x {_selected.Height} ({_selected.Left},{_selected.Top})";
|
||||||
|
using var font = new Font("Consolas", 9F);
|
||||||
|
var sz = e.Graphics.MeasureString(info, font);
|
||||||
|
float ty = r.Top - sz.Height - 2;
|
||||||
|
if (ty < 0) ty = r.Bottom + 2;
|
||||||
|
using var bg = new SolidBrush(Color.Lime);
|
||||||
|
e.Graphics.FillRectangle(bg, r.Left, ty, sz.Width, sz.Height);
|
||||||
|
e.Graphics.DrawString(info, font, Brushes.Black, r.Left, ty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rectangle RectangleFromPoints(Point a, Point b)
|
||||||
|
=> Rectangle.FromLTRB(
|
||||||
|
Math.Min(a.X, b.X), Math.Min(a.Y, b.Y),
|
||||||
|
Math.Max(a.X, b.X), Math.Max(a.Y, b.Y));
|
||||||
|
|
||||||
|
private Rectangle ScreenRectToClient(Rectangle screen)
|
||||||
|
{
|
||||||
|
var a = PointToClient(new Point(screen.Left, screen.Top));
|
||||||
|
var b = PointToClient(new Point(screen.Right, screen.Bottom));
|
||||||
|
return new Rectangle(a.X, a.Y, b.X - a.X, b.Y - a.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using anjianjingling.Common;
|
||||||
|
|
||||||
|
namespace anjianjingling.Forms
|
||||||
|
{
|
||||||
|
/// <summary>坐标取色器:置顶小窗,实时显示鼠标坐标与屏幕颜色,可复制坐标/颜色或写入日志。</summary>
|
||||||
|
public sealed class PickerForm : Form
|
||||||
|
{
|
||||||
|
private const int MagZoom = 7;
|
||||||
|
private const int SampleSize = 15;
|
||||||
|
|
||||||
|
private readonly LogBus _log;
|
||||||
|
private readonly System.Windows.Forms.Timer _timer = new() { Interval = 60 };
|
||||||
|
private readonly Label _lblPos = new();
|
||||||
|
private readonly Label _lblHex = new();
|
||||||
|
private readonly Label _lblRgb = new();
|
||||||
|
private readonly Panel _mag = new();
|
||||||
|
private readonly Panel _swatch = new();
|
||||||
|
private readonly Label _title = new();
|
||||||
|
private readonly Label _btnClose = new();
|
||||||
|
|
||||||
|
private Bitmap? _sample;
|
||||||
|
private Point _cursor;
|
||||||
|
private Color _color = Color.Black;
|
||||||
|
private Point _dragOffset;
|
||||||
|
private bool _dragging;
|
||||||
|
|
||||||
|
public PickerForm(LogBus log)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
|
||||||
|
Text = "坐标取色器";
|
||||||
|
FormBorderStyle = FormBorderStyle.None;
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
TopMost = true;
|
||||||
|
StartPosition = FormStartPosition.Manual;
|
||||||
|
ClientSize = new Size(288, 188);
|
||||||
|
BackColor = Color.FromArgb(30, 34, 42);
|
||||||
|
Font = new Font("Microsoft YaHei UI", 9F);
|
||||||
|
KeyPreview = true;
|
||||||
|
|
||||||
|
var start = Cursor.Position;
|
||||||
|
Location = new Point(
|
||||||
|
Math.Max(0, Math.Min(Screen.PrimaryScreen!.WorkingArea.Right - Width - 24, start.X + 40)),
|
||||||
|
Math.Max(0, Math.Min(Screen.PrimaryScreen!.WorkingArea.Bottom - Height - 24, start.Y + 40)));
|
||||||
|
|
||||||
|
// 顶栏
|
||||||
|
_title.Text = " 坐标取色器";
|
||||||
|
_title.Dock = DockStyle.None;
|
||||||
|
_title.SetBounds(0, 0, 240, 26);
|
||||||
|
_title.TextAlign = ContentAlignment.MiddleLeft;
|
||||||
|
_title.ForeColor = Color.FromArgb(220, 228, 240);
|
||||||
|
_title.Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
|
||||||
|
_title.BackColor = Color.FromArgb(42, 48, 60);
|
||||||
|
|
||||||
|
_btnClose.Text = "✕";
|
||||||
|
_btnClose.SetBounds(ClientSize.Width - 26, 0, 26, 26);
|
||||||
|
_btnClose.TextAlign = ContentAlignment.MiddleCenter;
|
||||||
|
_btnClose.ForeColor = Color.FromArgb(200, 210, 225);
|
||||||
|
_btnClose.BackColor = Color.FromArgb(42, 48, 60);
|
||||||
|
_btnClose.Cursor = Cursors.Hand;
|
||||||
|
_btnClose.Click += (s, e) => Close();
|
||||||
|
_btnClose.MouseEnter += (s, e) => _btnClose.BackColor = Color.FromArgb(190, 70, 60);
|
||||||
|
_btnClose.MouseLeave += (s, e) => _btnClose.BackColor = Color.FromArgb(42, 48, 60);
|
||||||
|
|
||||||
|
// 放大镜
|
||||||
|
_mag.SetBounds(10, 34, SampleSize * MagZoom, SampleSize * MagZoom);
|
||||||
|
_mag.BackColor = Color.FromArgb(20, 24, 30);
|
||||||
|
_mag.Paint += MagPaint;
|
||||||
|
|
||||||
|
// 色块
|
||||||
|
_swatch.SetBounds(124, 34, 62, 46);
|
||||||
|
_swatch.Paint += (s, e) =>
|
||||||
|
{
|
||||||
|
e.Graphics.Clear(_color);
|
||||||
|
using var pen = new Pen(Color.FromArgb(90, 98, 112));
|
||||||
|
e.Graphics.DrawRectangle(pen, 0, 0, _swatch.Width - 1, _swatch.Height - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文字
|
||||||
|
_lblPos.SetBounds(124, 86, 160, 18);
|
||||||
|
_lblHex.SetBounds(124, 106, 160, 18);
|
||||||
|
_lblRgb.SetBounds(124, 126, 160, 18);
|
||||||
|
foreach (var l in new[] { _lblPos, _lblHex, _lblRgb })
|
||||||
|
{
|
||||||
|
l.ForeColor = Color.FromArgb(214, 222, 235);
|
||||||
|
l.Font = new Font("Consolas", 9.5F);
|
||||||
|
}
|
||||||
|
_lblPos.Text = "坐标 -";
|
||||||
|
_lblHex.Text = "颜色 -";
|
||||||
|
_lblRgb.Text = "RGB -";
|
||||||
|
|
||||||
|
// 按钮
|
||||||
|
var bar = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
|
WrapContents = false
|
||||||
|
};
|
||||||
|
bar.SetBounds(10, 150, 268, 30);
|
||||||
|
bar.Controls.Add(MkBtn("复制坐标", () =>
|
||||||
|
Copy(_cursor.X + ", " + _cursor.Y)));
|
||||||
|
bar.Controls.Add(MkBtn("复制颜色", () =>
|
||||||
|
Copy($"{_color.R:X2}{_color.G:X2}{_color.B:X2}")));
|
||||||
|
bar.Controls.Add(MkBtn("写入日志", () =>
|
||||||
|
_log.Write($"取色:坐标 ({_cursor.X}, {_cursor.Y}) 颜色 #{_color.R:X2}{_color.G:X2}{_color.B:X2}")));
|
||||||
|
|
||||||
|
Controls.AddRange(new Control[] { _mag, _swatch, _lblPos, _lblHex, _lblRgb, bar, _title, _btnClose });
|
||||||
|
|
||||||
|
KeyDown += (s, e) => { if (e.KeyCode == Keys.Escape) Close(); };
|
||||||
|
MouseDown += DragStart;
|
||||||
|
_title.MouseDown += DragStart;
|
||||||
|
MouseMove += DragMove;
|
||||||
|
_title.MouseMove += DragMove;
|
||||||
|
MouseUp += (s, e) => _dragging = false;
|
||||||
|
|
||||||
|
_timer.Tick += (s, e) => Tick();
|
||||||
|
_timer.Start();
|
||||||
|
FormClosed += (s, e) => { _timer.Stop(); _sample?.Dispose(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button MkBtn(string text, Action onClick)
|
||||||
|
{
|
||||||
|
var b = new Button
|
||||||
|
{
|
||||||
|
Text = text, Width = 84, Height = 26, Margin = new Padding(0, 2, 4, 0),
|
||||||
|
FlatStyle = FlatStyle.Flat, BackColor = Color.FromArgb(52, 60, 74),
|
||||||
|
ForeColor = Color.FromArgb(222, 230, 242), Cursor = Cursors.Hand,
|
||||||
|
Font = new Font("Microsoft YaHei UI", 8.5F)
|
||||||
|
};
|
||||||
|
b.FlatAppearance.BorderColor = Color.FromArgb(70, 80, 96);
|
||||||
|
b.Click += (s, e) => onClick();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Copy(string text)
|
||||||
|
{
|
||||||
|
try { Clipboard.SetText(text); _log.Write("已复制:" + text); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DragStart(object? s, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Button != MouseButtons.Left) return;
|
||||||
|
_dragging = true;
|
||||||
|
_dragOffset = e.Location;
|
||||||
|
if (s != _title && s != this) _dragOffset = new Point(e.X + (s as Control)!.Left, e.Y + (s as Control)!.Top + 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DragMove(object? s, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_dragging) return;
|
||||||
|
var screen = s == this ? PointToScreen(e.Location) : (s as Control)!.PointToScreen(e.Location);
|
||||||
|
Location = new Point(screen.X - _dragOffset.X, screen.Y - _dragOffset.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
if (IsDisposed) return;
|
||||||
|
var p = Cursor.Position;
|
||||||
|
if (p == _cursor && _sample != null) return;
|
||||||
|
_cursor = p;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int half = SampleSize / 2;
|
||||||
|
_sample?.Dispose();
|
||||||
|
_sample = new Bitmap(SampleSize, SampleSize);
|
||||||
|
using (var g = Graphics.FromImage(_sample))
|
||||||
|
g.CopyFromScreen(p.X - half, p.Y - half, 0, 0, new Size(SampleSize, SampleSize));
|
||||||
|
|
||||||
|
_color = _sample.GetPixel(half, half);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lblPos.Text = $"坐标 {_cursor.X}, {_cursor.Y}";
|
||||||
|
_lblHex.Text = $"颜色 #{_color.R:X2}{_color.G:X2}{_color.B:X2}";
|
||||||
|
_lblRgb.Text = $"RGB {_color.R}, {_color.G}, {_color.B}";
|
||||||
|
_mag.Invalidate();
|
||||||
|
_swatch.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MagPaint(object? s, PaintEventArgs e)
|
||||||
|
{
|
||||||
|
e.Graphics.Clear(Color.FromArgb(20, 24, 30));
|
||||||
|
if (_sample == null) return;
|
||||||
|
e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
|
||||||
|
e.Graphics.PixelOffsetMode = PixelOffsetMode.Half;
|
||||||
|
e.Graphics.DrawImage(_sample, new Rectangle(0, 0, _mag.Width, _mag.Height));
|
||||||
|
|
||||||
|
// 准星
|
||||||
|
int c = _mag.Width / 2;
|
||||||
|
using var pen = new Pen(Color.FromArgb(220, 255, 80, 80)) { Width = 1 };
|
||||||
|
e.Graphics.DrawLine(pen, c, 0, c, _mag.Height);
|
||||||
|
e.Graphics.DrawLine(pen, 0, c, _mag.Width, c);
|
||||||
|
using var inner = new Pen(Color.White) { Width = 1 };
|
||||||
|
e.Graphics.DrawRectangle(inner, c - MagZoom / 2, c - MagZoom / 2, MagZoom, MagZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPaint(e);
|
||||||
|
using var pen = new Pen(Color.FromArgb(64, 72, 86));
|
||||||
|
e.Graphics.DrawRectangle(pen, 0, 0, ClientSize.Width - 1, ClientSize.Height - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using anjianjingling.Common;
|
||||||
|
using anjianjingling.Native;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace anjianjingling.Input
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 系统级键盘模拟(SendInput):单键、组合键、Unicode 文本。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class KeyboardInput
|
||||||
|
{
|
||||||
|
private static void SendKey(Win32.KEYBDINPUT ki)
|
||||||
|
{
|
||||||
|
var input = new Win32.INPUT
|
||||||
|
{
|
||||||
|
type = Win32.INPUT_KEYBOARD,
|
||||||
|
u = new Win32.INPUTUNION { ki = ki }
|
||||||
|
};
|
||||||
|
Win32.SendInput(1, new[] { input }, Marshal.SizeOf<Win32.INPUT>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>发送一次按键(按下并弹起)。</summary>
|
||||||
|
public void KeyPress(string keyName)
|
||||||
|
{
|
||||||
|
ushort vk = ResolveOrThrow(keyName);
|
||||||
|
KeyDownVk(vk);
|
||||||
|
Thread.Sleep(30);
|
||||||
|
KeyUpVk(vk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void KeyDown(string keyName) => KeyDownVk(ResolveOrThrow(keyName));
|
||||||
|
|
||||||
|
public void KeyUp(string keyName) => KeyUpVk(ResolveOrThrow(keyName));
|
||||||
|
|
||||||
|
private static void KeyDownVk(ushort vk)
|
||||||
|
{
|
||||||
|
ushort scan = (ushort)Win32.MapVirtualKey(vk, Win32.MAPVK_VK_TO_VSC);
|
||||||
|
uint flags = Win32.KEYEVENTF_SCANCODE;
|
||||||
|
if (VirtualKeys.IsExtended(vk)) flags |= Win32.KEYEVENTF_EXTENDEDKEY;
|
||||||
|
SendKey(new Win32.KEYBDINPUT { wVk = vk, wScan = scan, dwFlags = flags });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void KeyUpVk(ushort vk)
|
||||||
|
{
|
||||||
|
ushort scan = (ushort)Win32.MapVirtualKey(vk, Win32.MAPVK_VK_TO_VSC);
|
||||||
|
uint flags = Win32.KEYEVENTF_SCANCODE | Win32.KEYEVENTF_KEYUP;
|
||||||
|
if (VirtualKeys.IsExtended(vk)) flags |= Win32.KEYEVENTF_EXTENDEDKEY;
|
||||||
|
SendKey(new Win32.KEYBDINPUT { wVk = vk, wScan = scan, dwFlags = flags });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组合键,如 "Ctrl+A"、"Ctrl+Shift+Esc":修饰键依次按下,主键按下弹起,修饰键逆序弹起。
|
||||||
|
/// </summary>
|
||||||
|
public void HotKey(string expression)
|
||||||
|
{
|
||||||
|
var parts = expression.Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length == 0) throw new ArgumentException($"无法解析组合键:{expression}");
|
||||||
|
|
||||||
|
var vks = parts.Select(ResolveOrThrow).ToArray();
|
||||||
|
for (int i = 0; i < vks.Length - 1; i++) KeyDownVk(vks[i]);
|
||||||
|
Thread.Sleep(40);
|
||||||
|
KeyDownVk(vks[^1]);
|
||||||
|
Thread.Sleep(40);
|
||||||
|
KeyUpVk(vks[^1]);
|
||||||
|
for (int i = vks.Length - 2; i >= 0; i--)
|
||||||
|
{
|
||||||
|
Thread.Sleep(20);
|
||||||
|
KeyUpVk(vks[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 输入整段文本(含中文),使用 KEYEVENTF_UNICODE,不依赖键盘布局。
|
||||||
|
/// </summary>
|
||||||
|
public void TypeText(string text)
|
||||||
|
{
|
||||||
|
foreach (char c in text)
|
||||||
|
{
|
||||||
|
SendKey(new Win32.KEYBDINPUT { wScan = c, dwFlags = Win32.KEYEVENTF_UNICODE });
|
||||||
|
SendKey(new Win32.KEYBDINPUT { wScan = c, dwFlags = Win32.KEYEVENTF_UNICODE | Win32.KEYEVENTF_KEYUP });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ushort ResolveOrThrow(string name)
|
||||||
|
=> VirtualKeys.Resolve(name)
|
||||||
|
?? throw new ArgumentException($"未知按键名:\"{name}\"(可用如 A、F5、ENTER、SPACE、方向键等)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
using anjianjingling.Native;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace anjianjingling.Input
|
||||||
|
{
|
||||||
|
public enum MouseButton { Left, Right, Middle }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统级鼠标模拟(SendInput)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MouseInput
|
||||||
|
{
|
||||||
|
private static readonly Random Rng = new();
|
||||||
|
|
||||||
|
/// <summary>虚拟桌面(含多显示器)尺寸与原点。</summary>
|
||||||
|
private static Rectangle VirtualScreen()
|
||||||
|
{
|
||||||
|
int x = Win32.GetSystemMetrics(Win32.SM_XVIRTUALSCREEN);
|
||||||
|
int y = Win32.GetSystemMetrics(Win32.SM_YVIRTUALSCREEN);
|
||||||
|
int w = Win32.GetSystemMetrics(Win32.SM_CXVIRTUALSCREEN);
|
||||||
|
int h = Win32.GetSystemMetrics(Win32.SM_CYVIRTUALSCREEN);
|
||||||
|
return new Rectangle(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>把屏幕物理坐标换算为 SendInput 绝对坐标(0..65535,含虚拟桌面偏移)。</summary>
|
||||||
|
private static void ToAbsolute(int x, int y, out int ax, out int ay)
|
||||||
|
{
|
||||||
|
var vs = VirtualScreen();
|
||||||
|
ax = (int)((x - vs.Left) * 65535.0 / (vs.Width - 1));
|
||||||
|
ay = (int)((y - vs.Top) * 65535.0 / (vs.Height - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendSingle(Win32.MOUSEINPUT mi)
|
||||||
|
{
|
||||||
|
var input = new Win32.INPUT
|
||||||
|
{
|
||||||
|
type = Win32.INPUT_MOUSE,
|
||||||
|
u = new Win32.INPUTUNION { mi = mi }
|
||||||
|
};
|
||||||
|
Win32.SendInput(1, new[] { input }, Marshal.SizeOf<Win32.INPUT>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>移动鼠标到指定屏幕坐标(瞬时)。</summary>
|
||||||
|
public void MoveTo(int x, int y)
|
||||||
|
{
|
||||||
|
ToAbsolute(x, y, out int ax, out int ay);
|
||||||
|
SendSingle(new Win32.MOUSEINPUT
|
||||||
|
{
|
||||||
|
dx = ax,
|
||||||
|
dy = ay,
|
||||||
|
dwFlags = Win32.MOUSEEVENTF_MOVE | Win32.MOUSEEVENTF_ABSOLUTE | Win32.MOUSEEVENTF_VIRTUALDESK
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>相对移动。</summary>
|
||||||
|
public void MoveR(int dx, int dy)
|
||||||
|
{
|
||||||
|
var p = Position;
|
||||||
|
MoveTo(p.X + dx, p.Y + dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>带随机偏移的移动(拟人化)。</summary>
|
||||||
|
public void MoveToJitter(int x, int y, int jitterPx = 3)
|
||||||
|
{
|
||||||
|
MoveTo(x + Rng.Next(-jitterPx, jitterPx + 1),
|
||||||
|
y + Rng.Next(-jitterPx, jitterPx + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ButtonDown(MouseButton button)
|
||||||
|
=> SendSingle(new Win32.MOUSEINPUT { dwFlags = button switch
|
||||||
|
{
|
||||||
|
MouseButton.Left => Win32.MOUSEEVENTF_LEFTDOWN,
|
||||||
|
MouseButton.Right => Win32.MOUSEEVENTF_RIGHTDOWN,
|
||||||
|
MouseButton.Middle => Win32.MOUSEEVENTF_MIDDLEDOWN,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(button))
|
||||||
|
} });
|
||||||
|
|
||||||
|
public void ButtonUp(MouseButton button)
|
||||||
|
=> SendSingle(new Win32.MOUSEINPUT { dwFlags = button switch
|
||||||
|
{
|
||||||
|
MouseButton.Left => Win32.MOUSEEVENTF_LEFTUP,
|
||||||
|
MouseButton.Right => Win32.MOUSEEVENTF_RIGHTUP,
|
||||||
|
MouseButton.Middle => Win32.MOUSEEVENTF_MIDDLEUP,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(button))
|
||||||
|
} });
|
||||||
|
|
||||||
|
/// <summary>在当前位置点击;jitter=true 时落点带 ±3px 随机偏移(仅当指定坐标时)。</summary>
|
||||||
|
public void Click(MouseButton button, int? x = null, int? y = null, bool jitter = false)
|
||||||
|
{
|
||||||
|
if (x.HasValue && y.HasValue)
|
||||||
|
{
|
||||||
|
if (jitter) MoveToJitter(x.Value, y.Value);
|
||||||
|
else MoveTo(x.Value, y.Value);
|
||||||
|
Thread.Sleep(60 + Rng.Next(0, 50));
|
||||||
|
}
|
||||||
|
ButtonDown(button);
|
||||||
|
Thread.Sleep(40 + Rng.Next(0, 40));
|
||||||
|
ButtonUp(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>双击(左键)。</summary>
|
||||||
|
public void DoubleClick(int? x = null, int? y = null, bool jitter = false)
|
||||||
|
{
|
||||||
|
Click(MouseButton.Left, x, y, jitter);
|
||||||
|
Thread.Sleep(50 + Rng.Next(0, 60));
|
||||||
|
Click(MouseButton.Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>滚轮:正值向上滚,负值向下滚,单位为格(每格 120)。</summary>
|
||||||
|
public void Wheel(int delta)
|
||||||
|
{
|
||||||
|
SendSingle(new Win32.MOUSEINPUT
|
||||||
|
{
|
||||||
|
mouseData = (uint)(delta * Win32.WHEEL_DELTA),
|
||||||
|
dwFlags = Win32.MOUSEEVENTF_WHEEL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>从一点拖拽到另一点,按 duration 插值移动,模拟人手。</summary>
|
||||||
|
public void Drag(int x1, int y1, int x2, int y2, int durationMs = 400)
|
||||||
|
{
|
||||||
|
MoveTo(x1, y1);
|
||||||
|
Thread.Sleep(100);
|
||||||
|
ButtonDown(MouseButton.Left);
|
||||||
|
|
||||||
|
int steps = Math.Max(5, durationMs / 16);
|
||||||
|
for (int i = 1; i <= steps; i++)
|
||||||
|
{
|
||||||
|
double t = (double)i / steps;
|
||||||
|
// 轻微非线性,模拟加减速
|
||||||
|
double e = t * t * (3 - 2 * t);
|
||||||
|
int cx = (int)(x1 + (x2 - x1) * e);
|
||||||
|
int cy = (int)(y1 + (y2 - y1) * e);
|
||||||
|
MoveTo(cx, cy);
|
||||||
|
Thread.Sleep(16);
|
||||||
|
}
|
||||||
|
Thread.Sleep(80);
|
||||||
|
ButtonUp(MouseButton.Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>当前鼠标屏幕坐标。</summary>
|
||||||
|
public Point Position
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Win32.GetCursorPos(out var p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
namespace anjianjingling.Lua
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lua 脚本能力宿主:由主窗体实现,向 Lua 引擎提供系统级操作能力。
|
||||||
|
/// 所有方法均在后台执行线程上被调用。
|
||||||
|
/// </summary>
|
||||||
|
public interface ILuaHost
|
||||||
|
{
|
||||||
|
void Log(string msg);
|
||||||
|
|
||||||
|
/// <summary>是否启用拟人化(落点随机偏移 + 随机微延时)。</summary>
|
||||||
|
bool JitterEnabled { get; }
|
||||||
|
|
||||||
|
// ---- 鼠标 ----
|
||||||
|
void MouseMove(int x, int y);
|
||||||
|
void MouseMoveR(int dx, int dy);
|
||||||
|
void MouseClick(string button, int? x, int? y);
|
||||||
|
void MouseDoubleClick(int? x, int? y);
|
||||||
|
void MouseDown(string button);
|
||||||
|
void MouseUp(string button);
|
||||||
|
void MouseWheel(int delta);
|
||||||
|
void MouseDrag(int x1, int y1, int x2, int y2, int durationMs);
|
||||||
|
(int X, int Y) MousePosition();
|
||||||
|
|
||||||
|
// ---- 键盘 ----
|
||||||
|
void KeyPress(string key);
|
||||||
|
void KeyDown(string key);
|
||||||
|
void KeyUp(string key);
|
||||||
|
void HotKey(string expression);
|
||||||
|
void TypeText(string text);
|
||||||
|
|
||||||
|
// ---- 屏幕与视觉 ----
|
||||||
|
(int W, int H) ScreenSize();
|
||||||
|
string ColorAt(int x, int y);
|
||||||
|
(int X, int Y) FindColor(int x1, int y1, int x2, int y2, string hex, int tolerance);
|
||||||
|
(int X, int Y, double Score) FindImage(int x1, int y1, int x2, int y2, string templatePath, double similarity);
|
||||||
|
void ScreenShot(string path);
|
||||||
|
|
||||||
|
/// <summary>弹出框选截图,返回保存路径(用户取消返回 null)。</summary>
|
||||||
|
string? PickRegion(string? savePath);
|
||||||
|
|
||||||
|
// ---- 窗口 / 系统 ----
|
||||||
|
void Launch(string path, string? arguments);
|
||||||
|
bool ActivateWindow(string titleKeyword);
|
||||||
|
string[] ListWindows();
|
||||||
|
|
||||||
|
// ---- 剪贴板 ----
|
||||||
|
string ClipGet();
|
||||||
|
void ClipSet(string text);
|
||||||
|
|
||||||
|
// ---- 内嵌浏览器 ----
|
||||||
|
void BrowserOpen(string url);
|
||||||
|
bool BrowserClick(string selector);
|
||||||
|
bool BrowserType(string selector, string text);
|
||||||
|
bool BrowserWaitFor(string selector, int timeoutMs);
|
||||||
|
string BrowserGetText(string selector);
|
||||||
|
string BrowserExecJs(string js);
|
||||||
|
void BrowserShot(string path);
|
||||||
|
void BrowserBack();
|
||||||
|
void BrowserForward();
|
||||||
|
void BrowserReload();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace anjianjingling.Lua
|
||||||
|
{
|
||||||
|
public enum ArgKind
|
||||||
|
{
|
||||||
|
/// <summary>数字,原样输出。</summary>
|
||||||
|
Number,
|
||||||
|
/// <summary>字符串,生成时自动加引号转义。</summary>
|
||||||
|
Text,
|
||||||
|
/// <summary>键名 / 选择项,加引号。</summary>
|
||||||
|
Key,
|
||||||
|
/// <summary>表达式,原样输出。</summary>
|
||||||
|
Expr,
|
||||||
|
/// <summary>原样文本(变量名、注释),不加引号。</summary>
|
||||||
|
Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BlockShape
|
||||||
|
{
|
||||||
|
/// <summary>单行命令。</summary>
|
||||||
|
Simple,
|
||||||
|
/// <summary>if <条件> then … end(可切换 else 分支)。</summary>
|
||||||
|
If,
|
||||||
|
/// <summary>如果屏幕上存在图片 … then … end。</summary>
|
||||||
|
IfExists,
|
||||||
|
/// <summary>for i = 1, N do … end。</summary>
|
||||||
|
LoopN,
|
||||||
|
/// <summary>while <条件> do … end。</summary>
|
||||||
|
LoopWhile,
|
||||||
|
/// <summary>break。</summary>
|
||||||
|
Break
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record BlockArg(string Name, string Label, ArgKind Kind, string Default, string[]? Options = null);
|
||||||
|
|
||||||
|
public sealed class BlockDef
|
||||||
|
{
|
||||||
|
public required string Key { get; init; }
|
||||||
|
public required string Category { get; init; }
|
||||||
|
/// <summary>显示标题,含 {0} {1} 参数占位。</summary>
|
||||||
|
public required string Title { get; init; }
|
||||||
|
/// <summary>Lua 模板,含 {0} {1} 参数占位;容器块为起始行。</summary>
|
||||||
|
public string Template { get; init; } = "";
|
||||||
|
public BlockShape Shape { get; init; } = BlockShape.Simple;
|
||||||
|
public BlockArg[] Args { get; init; } = Array.Empty<BlockArg>();
|
||||||
|
public string Help { get; init; } = "";
|
||||||
|
|
||||||
|
public bool IsContainer => Shape is BlockShape.If or BlockShape.IfExists
|
||||||
|
or BlockShape.LoopN or BlockShape.LoopWhile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>画布上的一个积木实例。</summary>
|
||||||
|
public sealed class LuaBlock
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = "";
|
||||||
|
public List<string> Values { get; set; } = new();
|
||||||
|
/// <summary>容器块的子块。</summary>
|
||||||
|
public List<LuaBlock> Children { get; set; } = new();
|
||||||
|
/// <summary>If 块是否有 else 分支。</summary>
|
||||||
|
public bool HasElse { get; set; }
|
||||||
|
public List<LuaBlock> ElseChildren { get; set; } = new();
|
||||||
|
|
||||||
|
public LuaBlock Clone()
|
||||||
|
{
|
||||||
|
var b = new LuaBlock { Key = Key, HasElse = HasElse, Values = new List<string>(Values) };
|
||||||
|
foreach (var c in Children) b.Children.Add(c.Clone());
|
||||||
|
foreach (var c in ElseChildren) b.ElseChildren.Add(c.Clone());
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value(int i)
|
||||||
|
{
|
||||||
|
var def = BlockCatalog.Find(Key);
|
||||||
|
if (def == null || i >= def.Args.Length) return "";
|
||||||
|
return i < Values.Count && !string.IsNullOrEmpty(Values[i]) ? Values[i] : def.Args[i].Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>积木目录:定义表 + Lua 生成 + JSON 读写。</summary>
|
||||||
|
public static class BlockCatalog
|
||||||
|
{
|
||||||
|
public static readonly List<BlockDef> All = new();
|
||||||
|
private static readonly Dictionary<string, BlockDef> ByKey = new();
|
||||||
|
|
||||||
|
private static void Add(BlockDef d) { All.Add(d); ByKey[d.Key] = d; }
|
||||||
|
|
||||||
|
public static BlockDef? Find(string key) => ByKey.TryGetValue(key, out var d) ? d : null;
|
||||||
|
|
||||||
|
public static IEnumerable<string> Categories => All.Select(d => d.Category).Distinct();
|
||||||
|
|
||||||
|
static BlockCatalog()
|
||||||
|
{
|
||||||
|
// ==================== 流程 ====================
|
||||||
|
Add(new() { Key = "delay", Category = "流程", Title = "等待 {0} 毫秒", Template = "delay({0})",
|
||||||
|
Args = new[] { new BlockArg("ms", "毫秒", ArgKind.Number, "500") }, Help = "暂停指定时间" });
|
||||||
|
Add(new() { Key = "delayRandom", Category = "流程", Title = "随机等待 {0} ~ {1} 毫秒",
|
||||||
|
Template = "delayRandom({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("min", "最小", ArgKind.Number, "300"),
|
||||||
|
new BlockArg("max", "最大", ArgKind.Number, "800") } });
|
||||||
|
Add(new() { Key = "loopN", Category = "流程", Title = "循环 {0} 次", Shape = BlockShape.LoopN,
|
||||||
|
Args = new[] { new BlockArg("n", "次数", ArgKind.Number, "10") } });
|
||||||
|
Add(new() { Key = "loopWhile", Category = "流程", Title = "当 {0} 时循环", Shape = BlockShape.LoopWhile,
|
||||||
|
Args = new[] { new BlockArg("cond", "条件", ArgKind.Expr, "i < 10") } });
|
||||||
|
Add(new() { Key = "loopForever", Category = "流程", Title = "无限循环", Shape = BlockShape.LoopWhile });
|
||||||
|
Add(new() { Key = "if", Category = "流程", Title = "如果 {0}", Shape = BlockShape.If,
|
||||||
|
Args = new[] { new BlockArg("cond", "条件", ArgKind.Expr, "x >= 0") } });
|
||||||
|
Add(new() { Key = "break", Category = "流程", Title = "跳出循环", Shape = BlockShape.Break });
|
||||||
|
Add(new() { Key = "varSet", Category = "流程", Title = "设置变量 {0} = {1}", Template = "{0} = {1}",
|
||||||
|
Args = new[] { new BlockArg("name", "变量名", ArgKind.Raw, "x"),
|
||||||
|
new BlockArg("val", "值", ArgKind.Expr, "0") } });
|
||||||
|
Add(new() { Key = "varAdd", Category = "流程", Title = "变量 {0} 增加 {1}", Template = "{0} = {0} + {1}",
|
||||||
|
Args = new[] { new BlockArg("name", "变量名", ArgKind.Raw, "x"),
|
||||||
|
new BlockArg("val", "增量", ArgKind.Number, "1") } });
|
||||||
|
Add(new() { Key = "comment", Category = "流程", Title = "注释:{0}", Template = "-- {0}",
|
||||||
|
Args = new[] { new BlockArg("text", "内容", ArgKind.Raw, "说明") } });
|
||||||
|
// Lua 里无法识别的行,原样保留(脚本 ⇄ 积木互转时不丢代码)
|
||||||
|
Add(new() { Key = "lua", Category = "流程", Title = "自定义代码 {0}", Template = "{0}",
|
||||||
|
Args = new[] { new BlockArg("code", "代码", ArgKind.Raw, "-- 原生 Lua") },
|
||||||
|
Help = "脚本转换时无法识别的语句会变成此块,原样执行" });
|
||||||
|
|
||||||
|
// ==================== 鼠标 ====================
|
||||||
|
Add(new() { Key = "moveTo", Category = "鼠标", Title = "移动鼠标到 ({0}, {1})", Template = "moveTo({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
|
||||||
|
new BlockArg("y", "Y", ArgKind.Number, "300") } });
|
||||||
|
Add(new() { Key = "moveR", Category = "鼠标", Title = "相对移动 ({0}, {1})", Template = "moveR({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("dx", "X 偏移", ArgKind.Number, "10"),
|
||||||
|
new BlockArg("dy", "Y 偏移", ArgKind.Number, "10") } });
|
||||||
|
Add(new() { Key = "leftClickXY", Category = "鼠标", Title = "左键点击 ({0}, {1})",
|
||||||
|
Template = "leftClick({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
|
||||||
|
new BlockArg("y", "Y", ArgKind.Number, "300") } });
|
||||||
|
Add(new() { Key = "leftClick", Category = "鼠标", Title = "左键点击(当前位置)", Template = "leftClick()" });
|
||||||
|
Add(new() { Key = "rightClickXY", Category = "鼠标", Title = "右键点击 ({0}, {1})",
|
||||||
|
Template = "rightClick({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
|
||||||
|
new BlockArg("y", "Y", ArgKind.Number, "300") } });
|
||||||
|
Add(new() { Key = "rightClick", Category = "鼠标", Title = "右键点击(当前位置)", Template = "rightClick()" });
|
||||||
|
Add(new() { Key = "middleClick", Category = "鼠标", Title = "中键点击(当前位置)", Template = "middleClick()" });
|
||||||
|
Add(new() { Key = "doubleClick", Category = "鼠标", Title = "双击 ({0}, {1})", Template = "doubleClick({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"),
|
||||||
|
new BlockArg("y", "Y", ArgKind.Number, "300") } });
|
||||||
|
Add(new() { Key = "mouseDown", Category = "鼠标", Title = "按下鼠标 {0}", Template = "mouse.down({0})",
|
||||||
|
Args = new[] { new BlockArg("btn", "按键", ArgKind.Key, "left", new[] { "left", "right", "middle" }) } });
|
||||||
|
Add(new() { Key = "mouseUp", Category = "鼠标", Title = "弹起鼠标 {0}", Template = "mouse.up({0})",
|
||||||
|
Args = new[] { new BlockArg("btn", "按键", ArgKind.Key, "left", new[] { "left", "right", "middle" }) } });
|
||||||
|
Add(new() { Key = "wheel", Category = "鼠标", Title = "滚轮 {0} 格(正上负下)", Template = "wheel({0})",
|
||||||
|
Args = new[] { new BlockArg("n", "格数", ArgKind.Number, "-1") } });
|
||||||
|
Add(new() { Key = "drag", Category = "鼠标", Title = "拖拽 ({0},{1}) → ({2},{3}) 用时 {4} 毫秒",
|
||||||
|
Template = "drag({0}, {1}, {2}, {3}, {4})",
|
||||||
|
Args = new[] { new BlockArg("x1", "起点X", ArgKind.Number, "100"),
|
||||||
|
new BlockArg("y1", "起点Y", ArgKind.Number, "100"),
|
||||||
|
new BlockArg("x2", "终点X", ArgKind.Number, "600"),
|
||||||
|
new BlockArg("y2", "终点Y", ArgKind.Number, "400"),
|
||||||
|
new BlockArg("ms", "时长", ArgKind.Number, "500") } });
|
||||||
|
|
||||||
|
// ==================== 键盘 ====================
|
||||||
|
Add(new() { Key = "keyPress", Category = "键盘", Title = "按键 {0}", Template = "keyPress({0})",
|
||||||
|
Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "ENTER") },
|
||||||
|
Help = "ENTER / SPACE / TAB / ESC / F1~F12 / A~Z / LEFT / UP …" });
|
||||||
|
Add(new() { Key = "hotkey", Category = "键盘", Title = "组合键 {0}", Template = "hotkey({0})",
|
||||||
|
Args = new[] { new BlockArg("k", "组合", ArgKind.Key, "Ctrl+A") } });
|
||||||
|
Add(new() { Key = "typeText", Category = "键盘", Title = "输入文本 {0}", Template = "typeText({0})",
|
||||||
|
Args = new[] { new BlockArg("t", "文本", ArgKind.Text, "你好,世界") },
|
||||||
|
Help = "支持中文,使用 Unicode 注入,不受输入法影响" });
|
||||||
|
Add(new() { Key = "keyDown", Category = "键盘", Title = "按下键 {0}", Template = "keyDown({0})",
|
||||||
|
Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "SHIFT") } });
|
||||||
|
Add(new() { Key = "keyUp", Category = "键盘", Title = "弹起键 {0}", Template = "keyUp({0})",
|
||||||
|
Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "SHIFT") } });
|
||||||
|
|
||||||
|
// ==================== 视觉 ====================
|
||||||
|
Add(new() { Key = "findImage", Category = "视觉", Title = "找图 {0},相似度 {1} → 变量 {2}, {3}",
|
||||||
|
Template = "{2}, {3} = findImage({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"),
|
||||||
|
new BlockArg("sim", "相似度", ArgKind.Number, "0.85"),
|
||||||
|
new BlockArg("vx", "X 变量", ArgKind.Raw, "x"),
|
||||||
|
new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") },
|
||||||
|
Help = "找不到时变量为 -1;模板图片放程序目录 images/ 下" });
|
||||||
|
Add(new() { Key = "waitImage", Category = "视觉", Title = "等待图片 {0},超时 {1} 毫秒 → 变量 {2}, {3}",
|
||||||
|
Template = "{2}, {3} = waitImage({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"),
|
||||||
|
new BlockArg("ms", "超时", ArgKind.Number, "10000"),
|
||||||
|
new BlockArg("vx", "X 变量", ArgKind.Raw, "x"),
|
||||||
|
new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") } });
|
||||||
|
Add(new() { Key = "findColor", Category = "视觉", Title = "找色 {0},容差 {1} → 变量 {2}, {3}",
|
||||||
|
Template = "{2}, {3} = findColor({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("hex", "颜色", ArgKind.Text, "FF0000"),
|
||||||
|
new BlockArg("tol", "容差", ArgKind.Number, "10"),
|
||||||
|
new BlockArg("vx", "X 变量", ArgKind.Raw, "x"),
|
||||||
|
new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") } });
|
||||||
|
Add(new() { Key = "ifExists", Category = "视觉", Title = "如果屏幕上有图片 {0}(相似度 {1})",
|
||||||
|
Shape = BlockShape.IfExists,
|
||||||
|
Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"),
|
||||||
|
new BlockArg("sim", "相似度", ArgKind.Number, "0.85") } });
|
||||||
|
Add(new() { Key = "screenshot", Category = "视觉", Title = "截图保存到 {0}", Template = "screenshot({0})",
|
||||||
|
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "shot.png") } });
|
||||||
|
Add(new() { Key = "pickRegion", Category = "视觉", Title = "框选截图存为 {0}", Template = "screen.pick({0})",
|
||||||
|
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "images/btn.png") },
|
||||||
|
Help = "运行时弹窗框选一块屏幕区域,直接存成模板图" });
|
||||||
|
|
||||||
|
// ==================== 系统 ====================
|
||||||
|
Add(new() { Key = "run", Category = "系统", Title = "启动程序 {0}", Template = "run({0})",
|
||||||
|
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "notepad.exe") } });
|
||||||
|
Add(new() { Key = "activate", Category = "系统", Title = "激活窗口 {0}", Template = "activate({0})",
|
||||||
|
Args = new[] { new BlockArg("title", "标题关键字", ArgKind.Text, "记事本") } });
|
||||||
|
Add(new() { Key = "log", Category = "系统", Title = "日志 {0}", Template = "log({0})",
|
||||||
|
Args = new[] { new BlockArg("msg", "内容", ArgKind.Text, "提示信息") } });
|
||||||
|
Add(new() { Key = "clipSet", Category = "系统", Title = "复制到剪贴板 {0}", Template = "clipSet({0})",
|
||||||
|
Args = new[] { new BlockArg("text", "内容", ArgKind.Text, "") } });
|
||||||
|
Add(new() { Key = "stop", Category = "系统", Title = "停止脚本", Template = "stop()" });
|
||||||
|
|
||||||
|
// ==================== 浏览器 ====================
|
||||||
|
Add(new() { Key = "browserOpen", Category = "浏览器", Title = "打开网页 {0}", Template = "browser.open({0})",
|
||||||
|
Args = new[] { new BlockArg("url", "网址", ArgKind.Text, "https://www.baidu.com") } });
|
||||||
|
Add(new() { Key = "browserClick", Category = "浏览器", Title = "点击网页元素 {0}", Template = "browser.click({0})",
|
||||||
|
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, "#btn") } });
|
||||||
|
Add(new() { Key = "browserType", Category = "浏览器", Title = "网页元素 {0} 输入 {1}",
|
||||||
|
Template = "browser.type({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, "#input"),
|
||||||
|
new BlockArg("text", "文本", ArgKind.Text, "内容") } });
|
||||||
|
Add(new() { Key = "browserWait", Category = "浏览器", Title = "等待网页元素 {0},超时 {1} 毫秒",
|
||||||
|
Template = "browser.wait({0}, {1})",
|
||||||
|
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, ".loaded"),
|
||||||
|
new BlockArg("ms", "超时", ArgKind.Number, "10000") } });
|
||||||
|
Add(new() { Key = "browserText", Category = "浏览器", Title = "读取网页元素 {0} → 变量 {1}",
|
||||||
|
Template = "{1} = browser.text({0})",
|
||||||
|
Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, ".result"),
|
||||||
|
new BlockArg("v", "变量", ArgKind.Raw, "text") } });
|
||||||
|
Add(new() { Key = "browserJs", Category = "浏览器", Title = "执行 JS {0}", Template = "browser.js({0})",
|
||||||
|
Args = new[] { new BlockArg("js", "JS 代码", ArgKind.Text, "document.title") } });
|
||||||
|
Add(new() { Key = "browserShot", Category = "浏览器", Title = "网页截图 {0}", Template = "browser.shot({0})",
|
||||||
|
Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "web.png") } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 标题渲染 ====================
|
||||||
|
|
||||||
|
public static string RenderTitle(LuaBlock b)
|
||||||
|
{
|
||||||
|
var def = Find(b.Key);
|
||||||
|
if (def == null) return "未知积木:" + b.Key;
|
||||||
|
var args = new object[def.Args.Length];
|
||||||
|
for (int i = 0; i < def.Args.Length; i++)
|
||||||
|
{
|
||||||
|
string v = b.Value(i);
|
||||||
|
if (def.Args[i].Kind == ArgKind.Text) v = "\"" + v + "\"";
|
||||||
|
args[i] = v;
|
||||||
|
}
|
||||||
|
try { return string.Format(def.Title, args); }
|
||||||
|
catch { return def.Title; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lua 生成 ====================
|
||||||
|
|
||||||
|
public static string ToLua(IEnumerable<LuaBlock> blocks)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
Emit(blocks, 0, sb);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Emit(IEnumerable<LuaBlock> blocks, int level, StringBuilder sb)
|
||||||
|
{
|
||||||
|
string pad = new(' ', level * 4);
|
||||||
|
foreach (var b in blocks)
|
||||||
|
{
|
||||||
|
var def = Find(b.Key);
|
||||||
|
if (def == null) continue;
|
||||||
|
|
||||||
|
switch (def.Shape)
|
||||||
|
{
|
||||||
|
case BlockShape.If:
|
||||||
|
sb.AppendLine($"{pad}if {ArgText(def, b, 0)} then");
|
||||||
|
Emit(b.Children, level + 1, sb);
|
||||||
|
if (b.HasElse)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{pad}else");
|
||||||
|
Emit(b.ElseChildren, level + 1, sb);
|
||||||
|
}
|
||||||
|
sb.AppendLine($"{pad}end");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BlockShape.IfExists:
|
||||||
|
sb.AppendLine($"{pad}do");
|
||||||
|
sb.AppendLine($"{pad} local __ix, __iy = findImage({ArgText(def, b, 0)}, {ArgText(def, b, 1)})");
|
||||||
|
sb.AppendLine($"{pad} if __ix >= 0 then");
|
||||||
|
Emit(b.Children, level + 2, sb);
|
||||||
|
if (b.HasElse)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{pad} else");
|
||||||
|
Emit(b.ElseChildren, level + 2, sb);
|
||||||
|
}
|
||||||
|
sb.AppendLine($"{pad} end");
|
||||||
|
sb.AppendLine($"{pad}end");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BlockShape.LoopN:
|
||||||
|
sb.AppendLine($"{pad}for i = 1, {ArgText(def, b, 0)} do");
|
||||||
|
Emit(b.Children, level + 1, sb);
|
||||||
|
sb.AppendLine($"{pad}end");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BlockShape.LoopWhile:
|
||||||
|
string cond = def.Args.Length == 0 ? "true" : ArgText(def, b, 0);
|
||||||
|
sb.AppendLine($"{pad}while {cond} do");
|
||||||
|
Emit(b.Children, level + 1, sb);
|
||||||
|
sb.AppendLine($"{pad}end");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BlockShape.Break:
|
||||||
|
sb.AppendLine($"{pad}break");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (string.IsNullOrEmpty(def.Template)) break;
|
||||||
|
var vals = new object[def.Args.Length];
|
||||||
|
for (int i = 0; i < def.Args.Length; i++) vals[i] = ArgText(def, b, i);
|
||||||
|
string line;
|
||||||
|
try { line = string.Format(def.Template, vals); }
|
||||||
|
catch { line = def.Template; }
|
||||||
|
sb.AppendLine(pad + line);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ArgText(BlockDef def, LuaBlock b, int i)
|
||||||
|
{
|
||||||
|
if (i >= def.Args.Length) return "";
|
||||||
|
var arg = def.Args[i];
|
||||||
|
string v = b.Value(i);
|
||||||
|
return arg.Kind switch
|
||||||
|
{
|
||||||
|
ArgKind.Number => string.IsNullOrWhiteSpace(v) ? "0" : v.Trim(),
|
||||||
|
ArgKind.Expr or ArgKind.Raw => v,
|
||||||
|
_ => Quote(v)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Quote(string s)
|
||||||
|
=> "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"")
|
||||||
|
.Replace("\r", "").Replace("\n", "\\n") + "\"";
|
||||||
|
|
||||||
|
// ==================== Lua → 积木(双向转换) ====================
|
||||||
|
|
||||||
|
private sealed class Frame
|
||||||
|
{
|
||||||
|
public LuaBlock Block = null!;
|
||||||
|
public List<LuaBlock> ParentList = null!;
|
||||||
|
public int Ends; // 还差几个 end 才结束此块
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TplInfo
|
||||||
|
{
|
||||||
|
public BlockDef Def = null!;
|
||||||
|
public string Call = "";
|
||||||
|
public int RetCount;
|
||||||
|
public List<int> ArgIdx = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TplInfo>? _tplCache;
|
||||||
|
private static List<TplInfo> TplCache => _tplCache ??= BuildTplCache();
|
||||||
|
|
||||||
|
private static List<TplInfo> BuildTplCache()
|
||||||
|
{
|
||||||
|
var list = new List<TplInfo>();
|
||||||
|
foreach (var def in All)
|
||||||
|
{
|
||||||
|
if (def.Shape != BlockShape.Simple || def.Template.Length == 0) continue;
|
||||||
|
|
||||||
|
string callPart = def.Template, retPart = "";
|
||||||
|
int eq = def.Template.IndexOf(" = ", StringComparison.Ordinal);
|
||||||
|
if (eq >= 0)
|
||||||
|
{
|
||||||
|
retPart = def.Template.Substring(0, eq);
|
||||||
|
callPart = def.Template.Substring(eq + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = Regex.Match(callPart, @"^([A-Za-z_][\w\.]*)\s*\((.*)\)$");
|
||||||
|
if (!m.Success) continue;
|
||||||
|
|
||||||
|
// 左侧只允许纯占位符({2}, {3}),右侧只允许 "a, b" 形式的占位符列表
|
||||||
|
string retFixed = Regex.Replace(retPart, @"\{\d+\}", "").Trim().Replace(",", "").Replace(" ", "");
|
||||||
|
if (retPart.Length > 0 && retFixed.Length > 0) continue;
|
||||||
|
string argFixed = Regex.Replace(m.Groups[2].Value, @"\{\d+\}", "").Trim().Replace(",", "").Replace(" ", "");
|
||||||
|
if (argFixed.Length > 0) continue;
|
||||||
|
if (retPart.Length > 0 && !Regex.IsMatch(retPart, @"\{\d+\}")) continue;
|
||||||
|
|
||||||
|
var info = new TplInfo { Def = def, Call = m.Groups[1].Value };
|
||||||
|
if (retPart.Length > 0)
|
||||||
|
info.RetCount = Regex.Matches(retPart, @"\{\d+\}").Count;
|
||||||
|
foreach (Match ph in Regex.Matches(m.Groups[2].Value, @"\{(\d+)\}"))
|
||||||
|
info.ArgIdx.Add(int.Parse(ph.Groups[1].Value));
|
||||||
|
|
||||||
|
list.Add(info);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>把 Lua 脚本解析成积木。认识目录里的全部命令,认不出的行进「自定义代码」块。</summary>
|
||||||
|
public static List<LuaBlock> FromLua(string code)
|
||||||
|
{
|
||||||
|
var root = new List<LuaBlock>();
|
||||||
|
var stack = new Stack<Frame>();
|
||||||
|
var cur = root;
|
||||||
|
|
||||||
|
string[] lines = (code ?? "").Replace("\r\n", "\n").Split('\n');
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
string raw = lines[i].Trim();
|
||||||
|
if (raw.Length == 0) continue;
|
||||||
|
|
||||||
|
if (raw.StartsWith("--"))
|
||||||
|
{
|
||||||
|
cur.Add(new LuaBlock { Key = "comment", Values = { raw.Substring(2).Trim() } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string line = StripComment(raw).Trim();
|
||||||
|
if (line.Length == 0) continue;
|
||||||
|
|
||||||
|
// ifExists 生成的三行结构:do / local __ix,__iy = findImage(…) / if __ix >= 0 then
|
||||||
|
if (line == "do" && i + 2 < lines.Length)
|
||||||
|
{
|
||||||
|
string l1 = StripComment(lines[i + 1]).Trim();
|
||||||
|
string l2 = StripComment(lines[i + 2]).Trim();
|
||||||
|
var m1 = Regex.Match(l1, @"^local\s+__\w+\s*,\s*__\w+\s*=\s*findImage\s*\((.*)\)$");
|
||||||
|
if (m1.Success && Regex.IsMatch(l2, @"^if\s+__\w+\s*>=\s*0\s+then$"))
|
||||||
|
{
|
||||||
|
var b = new LuaBlock { Key = "ifExists" };
|
||||||
|
FillArgs(b, SplitArgs(m1.Groups[1].Value), new List<int> { 0, 1 });
|
||||||
|
cur.Add(b);
|
||||||
|
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 2 });
|
||||||
|
cur = b.Children;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line == "end")
|
||||||
|
{
|
||||||
|
if (stack.Count > 0)
|
||||||
|
{
|
||||||
|
stack.Peek().Ends--;
|
||||||
|
while (stack.Count > 0 && stack.Peek().Ends <= 0)
|
||||||
|
cur = stack.Pop().ParentList;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line == "else" || line.StartsWith("elseif "))
|
||||||
|
{
|
||||||
|
bool handled = false;
|
||||||
|
if (stack.Count > 0)
|
||||||
|
{
|
||||||
|
var top = stack.Peek();
|
||||||
|
if (top.Block.Key is "if" or "ifExists")
|
||||||
|
{
|
||||||
|
if (line == "else" && !top.Block.HasElse)
|
||||||
|
{
|
||||||
|
top.Block.HasElse = true;
|
||||||
|
cur = top.Block.ElseChildren;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("elseif "))
|
||||||
|
{
|
||||||
|
// elseif → else 里嵌一个 if,共用原来的 end
|
||||||
|
string cond = Regex.Match(line, @"^elseif\s+(.+?)\s+then$").Groups[1].Value;
|
||||||
|
if (cond.Length > 0)
|
||||||
|
{
|
||||||
|
top.Ends--;
|
||||||
|
top.Block.HasElse = true;
|
||||||
|
var nested = new LuaBlock { Key = "if", Values = { cond.Trim() } };
|
||||||
|
cur.Add(nested);
|
||||||
|
stack.Push(new Frame { Block = nested, ParentList = cur, Ends = 1 });
|
||||||
|
cur = nested.Children;
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled) cur.Add(Custom(raw));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mFor = Regex.Match(line, @"^for\s+\w+\s*=\s*([^,]+?)\s*,\s*(.+?)\s+do$");
|
||||||
|
if (mFor.Success)
|
||||||
|
{
|
||||||
|
var b = new LuaBlock { Key = "loopN", Values = { mFor.Groups[2].Value.Trim() } };
|
||||||
|
cur.Add(b);
|
||||||
|
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 });
|
||||||
|
cur = b.Children;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mWhile = Regex.Match(line, @"^while\s+(.+?)\s+do$");
|
||||||
|
if (mWhile.Success)
|
||||||
|
{
|
||||||
|
string cond = mWhile.Groups[1].Value.Trim();
|
||||||
|
var b = cond == "true"
|
||||||
|
? new LuaBlock { Key = "loopForever" }
|
||||||
|
: new LuaBlock { Key = "loopWhile", Values = { cond } };
|
||||||
|
cur.Add(b);
|
||||||
|
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 });
|
||||||
|
cur = b.Children;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mIf = Regex.Match(line, @"^if\s+(.+?)\s+then$");
|
||||||
|
if (mIf.Success)
|
||||||
|
{
|
||||||
|
var b = new LuaBlock { Key = "if", Values = { mIf.Groups[1].Value.Trim() } };
|
||||||
|
cur.Add(b);
|
||||||
|
stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 });
|
||||||
|
cur = b.Children;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line == "break") { cur.Add(new LuaBlock { Key = "break" }); continue; }
|
||||||
|
|
||||||
|
var tpl = MatchTemplate(line);
|
||||||
|
if (tpl != null) { cur.Add(tpl); continue; }
|
||||||
|
|
||||||
|
var mAdd = Regex.Match(line, @"^(\w+)\s*=\s*\1\s*\+\s*(.+)$");
|
||||||
|
if (mAdd.Success)
|
||||||
|
{
|
||||||
|
cur.Add(new LuaBlock { Key = "varAdd", Values = { mAdd.Groups[1].Value, mAdd.Groups[2].Value.Trim() } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var mSet = Regex.Match(line, @"^([A-Za-z_]\w*)\s*=\s*(.+)$");
|
||||||
|
if (mSet.Success)
|
||||||
|
{
|
||||||
|
cur.Add(new LuaBlock { Key = "varSet", Values = { mSet.Groups[1].Value, mSet.Groups[2].Value.Trim() } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cur.Add(Custom(raw));
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LuaBlock Custom(string line) => new() { Key = "lua", Values = { line.Trim() } };
|
||||||
|
|
||||||
|
/// <summary>去掉行尾注释(不切字符串里的 --)。</summary>
|
||||||
|
private static string StripComment(string s)
|
||||||
|
{
|
||||||
|
bool inStr = false;
|
||||||
|
for (int i = 0; i < s.Length - 1; i++)
|
||||||
|
{
|
||||||
|
char c = s[i];
|
||||||
|
if (c == '\\' && inStr) { i++; continue; }
|
||||||
|
if (c == '"') inStr = !inStr;
|
||||||
|
else if (!inStr && c == '-' && i + 1 < s.Length && s[i + 1] == '-')
|
||||||
|
return s.Substring(0, i);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LuaBlock? MatchTemplate(string line)
|
||||||
|
{
|
||||||
|
foreach (var info in TplCache)
|
||||||
|
{
|
||||||
|
string callPat = Regex.Escape(info.Call).Replace(@"\.", @"\.");
|
||||||
|
string pat = info.RetCount switch
|
||||||
|
{
|
||||||
|
0 => "^(?:local\\s+)?" + callPat + @"\s*\((.*)\)$",
|
||||||
|
1 => "^(?:local\\s+)?([A-Za-z_][\\w\\.]*)\\s*=\\s*" + callPat + @"\s*\((.*)\)$",
|
||||||
|
_ => "^(?:local\\s+)?([A-Za-z_][\\w\\.]*\\s*,\\s*[A-Za-z_][\\w\\.]*)\\s*=\\s*" + callPat + @"\s*\((.*)\)$"
|
||||||
|
};
|
||||||
|
var m = Regex.Match(line, pat);
|
||||||
|
if (!m.Success) continue;
|
||||||
|
|
||||||
|
var args = SplitArgs(info.RetCount == 0 ? m.Groups[1].Value : m.Groups[2].Value);
|
||||||
|
if (args.Count != info.ArgIdx.Count) continue;
|
||||||
|
|
||||||
|
// 返回值变量数必须一致
|
||||||
|
if (info.RetCount > 0)
|
||||||
|
{
|
||||||
|
int lhs = m.Groups[1].Value.Split(',').Length;
|
||||||
|
if (lhs != info.RetCount) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var b = new LuaBlock { Key = info.Def.Key };
|
||||||
|
FillArgs(b, args, info.ArgIdx);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FillArgs(LuaBlock b, List<string> args, List<int> slotIdx)
|
||||||
|
{
|
||||||
|
var def = Find(b.Key)!;
|
||||||
|
for (int k = 0; k < slotIdx.Count && k < args.Count; k++)
|
||||||
|
{
|
||||||
|
int slot = slotIdx[k];
|
||||||
|
while (b.Values.Count < def.Args.Length) b.Values.Add("");
|
||||||
|
b.Values[slot] = def.Args[slot].Kind is ArgKind.Text or ArgKind.Key
|
||||||
|
? Unquote(args[k]) : args[k].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按顶层逗号拆参数(跳过字符串 / 括号内的逗号)。</summary>
|
||||||
|
private static List<string> SplitArgs(string s)
|
||||||
|
{
|
||||||
|
var list = new List<string>();
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return list;
|
||||||
|
int depth = 0;
|
||||||
|
bool inStr = false;
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var c in s)
|
||||||
|
{
|
||||||
|
if (inStr)
|
||||||
|
{
|
||||||
|
sb.Append(c);
|
||||||
|
if (c == '"') inStr = false;
|
||||||
|
else if (c == '\\') { sb.Length--; /* 下一个字符原样进 */ }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case '"': inStr = true; sb.Append(c); break;
|
||||||
|
case '(' or '[' or '{': depth++; sb.Append(c); break;
|
||||||
|
case ')' or ']' or '}': depth--; sb.Append(c); break;
|
||||||
|
case ',' when depth == 0:
|
||||||
|
list.Add(sb.ToString().Trim()); sb.Clear(); break;
|
||||||
|
default: sb.Append(c); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sb.Length > 0) list.Add(sb.ToString().Trim());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Unquote(string s)
|
||||||
|
{
|
||||||
|
s = s.Trim();
|
||||||
|
if (s.Length >= 2 && ((s[0] == '"' && s[^1] == '"') || (s[0] == '\'' && s[^1] == '\'')))
|
||||||
|
{
|
||||||
|
s = s.Substring(1, s.Length - 2);
|
||||||
|
s = s.Replace("\\\"", "\"").Replace("\\'", "'")
|
||||||
|
.Replace("\\\\", "\\").Replace("\\n", "\n").Replace("\\t", "\t");
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== JSON ====================
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
public static string ToJson(IEnumerable<LuaBlock> blocks) => JsonSerializer.Serialize(blocks, JsonOpt);
|
||||||
|
|
||||||
|
public static List<LuaBlock> FromJson(string json)
|
||||||
|
=> JsonSerializer.Deserialize<List<LuaBlock>>(json) ?? new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
using MoonSharp.Interpreter;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace anjianjingling.Lua
|
||||||
|
{
|
||||||
|
public enum LuaState { Idle, Running, Paused }
|
||||||
|
|
||||||
|
/// <summary>脚本中止信号(停止 / 急停 / 主动 stop)。</summary>
|
||||||
|
internal sealed class AbortSignal : Exception
|
||||||
|
{
|
||||||
|
public bool ByPanic { get; }
|
||||||
|
public AbortSignal(bool byPanic) : base(byPanic ? "鼠标触及屏幕左上角" : "脚本已停止")
|
||||||
|
=> ByPanic = byPanic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lua 脚本执行器:MoonSharp 宿主 + 暂停/停止/急停 + 任务队列。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LuaEngine
|
||||||
|
{
|
||||||
|
private readonly ILuaHost _host;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private readonly ManualResetEventSlim _pauseGate = new(true);
|
||||||
|
private Task? _task;
|
||||||
|
private volatile bool _paused;
|
||||||
|
private volatile bool _byPanic;
|
||||||
|
private long _lastPanicCheckTick;
|
||||||
|
|
||||||
|
public LuaState State { get; private set; } = LuaState.Idle;
|
||||||
|
public event Action<LuaState>? StateChanged;
|
||||||
|
|
||||||
|
/// <summary>执行速度倍率:只影响 delay 等待时长。</summary>
|
||||||
|
public double Speed { get; set; } = 1.0;
|
||||||
|
|
||||||
|
public LuaEngine(ILuaHost host) => _host = host;
|
||||||
|
|
||||||
|
// ===================== 语法检查 =====================
|
||||||
|
|
||||||
|
/// <summary>只编译不执行;返回 null 表示语法通过。</summary>
|
||||||
|
public string? Validate(string code)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var script = CreateScript();
|
||||||
|
script.LoadString(code, null, "脚本");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (SyntaxErrorException ex) { return Localize(ex.Message); }
|
||||||
|
catch (Exception ex) { return ex.Message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 运行 =====================
|
||||||
|
|
||||||
|
/// <summary>运行单段脚本(一次)。</summary>
|
||||||
|
public void Run(string code)
|
||||||
|
{
|
||||||
|
if (State != LuaState.Idle) { _host.Log("已有脚本在运行"); return; }
|
||||||
|
|
||||||
|
DynValue chunk;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var script = CreateScript();
|
||||||
|
chunk = script.LoadString(code, null, "脚本");
|
||||||
|
}
|
||||||
|
catch (SyntaxErrorException ex)
|
||||||
|
{
|
||||||
|
_host.Log("语法错误:" + Localize(ex.Message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RunBackground(() =>
|
||||||
|
{
|
||||||
|
chunk.Function.Call();
|
||||||
|
_host.Log("脚本执行完毕");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按顺序执行勾选的任务;每个任务按自身循环设置重复。</summary>
|
||||||
|
public void RunQueue(IReadOnlyList<LuaTask> tasks)
|
||||||
|
{
|
||||||
|
if (State != LuaState.Idle) { _host.Log("已有脚本在运行"); return; }
|
||||||
|
|
||||||
|
var enabled = tasks.Where(t => t.Enabled).ToList();
|
||||||
|
if (enabled.Count == 0) { _host.Log("没有勾选的任务"); return; }
|
||||||
|
|
||||||
|
// 启动前统一编译,提前暴露语法错误
|
||||||
|
var prepared = new List<(LuaTask Task, DynValue Chunk)>();
|
||||||
|
foreach (var t in enabled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var script = CreateScript();
|
||||||
|
prepared.Add((t, script.LoadString(t.Script, null, t.Name)));
|
||||||
|
}
|
||||||
|
catch (SyntaxErrorException ex)
|
||||||
|
{
|
||||||
|
_host.Log($"任务「{t.Name}」语法错误:{Localize(ex.Message)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RunBackground(() =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < prepared.Count; i++)
|
||||||
|
{
|
||||||
|
var (task, chunk) = prepared[i];
|
||||||
|
_host.Log($"▶ 开始任务 {i + 1}/{prepared.Count}:{task.Name}({task.ModeText})");
|
||||||
|
|
||||||
|
int round = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Tick();
|
||||||
|
if (!task.Infinite && round >= task.LoopCount) break;
|
||||||
|
|
||||||
|
chunk.Function.Call();
|
||||||
|
round++;
|
||||||
|
|
||||||
|
bool more = task.Infinite || round < task.LoopCount;
|
||||||
|
if (more && task.IntervalMs > 0) Sleep(task.IntervalMs);
|
||||||
|
}
|
||||||
|
_host.Log($"✔ 任务完成:{task.Name}({round} 轮)");
|
||||||
|
}
|
||||||
|
_host.Log("★ 任务队列全部执行完毕");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunBackground(Action body)
|
||||||
|
{
|
||||||
|
_paused = false;
|
||||||
|
_byPanic = false;
|
||||||
|
_pauseGate.Set();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
SetState(LuaState.Running);
|
||||||
|
_task = Task.Run(() =>
|
||||||
|
{
|
||||||
|
try { body(); }
|
||||||
|
catch (AbortSignal ex)
|
||||||
|
{
|
||||||
|
_host.Log(ex.ByPanic ? "■ 鼠标触及屏幕左上角,紧急停止" : "■ 脚本已停止");
|
||||||
|
}
|
||||||
|
catch (SyntaxErrorException ex) { _host.Log("语法错误:" + Localize(ex.Message)); }
|
||||||
|
catch (ScriptRuntimeException ex) { _host.Log("运行错误:" + Localize(ex.Message)); }
|
||||||
|
catch (InterpreterException ex) { _host.Log("运行错误:" + Localize(ex.Message)); }
|
||||||
|
catch (Exception ex) { _host.Log("运行错误:" + ex.Message); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
_pauseGate.Set();
|
||||||
|
_paused = false;
|
||||||
|
SetState(LuaState.Idle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Script CreateScript()
|
||||||
|
{
|
||||||
|
var script = new Script(CoreModules.Preset_HardSandbox
|
||||||
|
| CoreModules.String | CoreModules.Math
|
||||||
|
| CoreModules.Table | CoreModules.Coroutine);
|
||||||
|
script.Options.DebugPrint = s => _host.Log(s);
|
||||||
|
ScriptApi.Bind(script, this, _host);
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 控制 =====================
|
||||||
|
|
||||||
|
public void PauseOrResume()
|
||||||
|
{
|
||||||
|
if (State == LuaState.Running)
|
||||||
|
{
|
||||||
|
_paused = true;
|
||||||
|
_pauseGate.Reset();
|
||||||
|
_host.Log("‖ 已暂停(F9 继续)");
|
||||||
|
SetState(LuaState.Paused);
|
||||||
|
}
|
||||||
|
else if (State == LuaState.Paused)
|
||||||
|
{
|
||||||
|
_paused = false;
|
||||||
|
_pauseGate.Set();
|
||||||
|
_host.Log("▶ 继续执行");
|
||||||
|
SetState(LuaState.Running);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (State == LuaState.Idle) return;
|
||||||
|
_byPanic = false;
|
||||||
|
_paused = false;
|
||||||
|
_pauseGate.Set();
|
||||||
|
_cts?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetState(LuaState s)
|
||||||
|
{
|
||||||
|
State = s;
|
||||||
|
StateChanged?.Invoke(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 中断点(API 回调内调用)=====================
|
||||||
|
|
||||||
|
internal void Tick()
|
||||||
|
{
|
||||||
|
var cts = _cts;
|
||||||
|
if (cts is { IsCancellationRequested: true }) throw new AbortSignal(_byPanic);
|
||||||
|
|
||||||
|
if (_paused)
|
||||||
|
{
|
||||||
|
_pauseGate.Wait();
|
||||||
|
if (cts is { IsCancellationRequested: true }) throw new AbortSignal(_byPanic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 急停检查节流到 100ms 一次,避免每条命令都调系统 API
|
||||||
|
long now = Environment.TickCount64;
|
||||||
|
if (now - _lastPanicCheckTick < 100) return;
|
||||||
|
_lastPanicCheckTick = now;
|
||||||
|
|
||||||
|
Native.Win32.GetCursorPos(out var p);
|
||||||
|
if (p.X > 0 || p.Y > 0) return;
|
||||||
|
|
||||||
|
Thread.Sleep(120);
|
||||||
|
Native.Win32.GetCursorPos(out var p2);
|
||||||
|
if (p2.X <= 0 && p2.Y <= 0)
|
||||||
|
{
|
||||||
|
_byPanic = true;
|
||||||
|
_cts?.Cancel();
|
||||||
|
throw new AbortSignal(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>可中断、可暂停的延时(分片 20ms,支持倍速)。</summary>
|
||||||
|
internal void Sleep(double ms)
|
||||||
|
{
|
||||||
|
ms = Math.Max(0, ms / Math.Max(0.05, Speed));
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
while (sw.Elapsed.TotalMilliseconds < ms)
|
||||||
|
{
|
||||||
|
Tick();
|
||||||
|
int slice = (int)Math.Min(20, Math.Max(1, ms - sw.Elapsed.TotalMilliseconds));
|
||||||
|
_pauseGate.Wait(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 错误信息中文化 =====================
|
||||||
|
|
||||||
|
private static readonly (string From, string To)[] Dict =
|
||||||
|
{
|
||||||
|
("unexpected symbol near '<eof>'", "语句未写完(意外到达文件结尾)"),
|
||||||
|
("unexpected symbol", "意外的符号"),
|
||||||
|
("'end' expected", "缺少 end"),
|
||||||
|
("'=' expected", "缺少 ="),
|
||||||
|
("'then' expected", "缺少 then"),
|
||||||
|
("'do' expected", "缺少 do"),
|
||||||
|
("unfinished string", "字符串未闭合"),
|
||||||
|
("malformed number", "数字格式错误"),
|
||||||
|
("attempt to index a nil value", "访问了空值(nil)"),
|
||||||
|
("attempt to call a nil value", "调用了不存在的函数"),
|
||||||
|
("attempt to perform arithmetic", "算术运算类型错误"),
|
||||||
|
("attempt to compare", "比较运算类型错误"),
|
||||||
|
("index out of range", "索引越界"),
|
||||||
|
("cannot convert", "类型转换失败"),
|
||||||
|
("invalid arguments", "参数不合法"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string Localize(string msg)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(msg)) return msg;
|
||||||
|
foreach (var (from, to) in Dict)
|
||||||
|
if (msg.Contains(from, StringComparison.OrdinalIgnoreCase))
|
||||||
|
msg = msg.Replace(from, to, StringComparison.OrdinalIgnoreCase);
|
||||||
|
return msg.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace anjianjingling.Lua
|
||||||
|
{
|
||||||
|
/// <summary>任务列表中的一个脚本任务。</summary>
|
||||||
|
public sealed class LuaTask
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "新任务";
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>true=无限循环(会阻塞后续任务,需手动停止)。</summary>
|
||||||
|
public bool Infinite { get; set; } = false;
|
||||||
|
|
||||||
|
public int LoopCount { get; set; } = 1;
|
||||||
|
public int IntervalMs { get; set; } = 1000;
|
||||||
|
public string Script { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>积木画布 JSON(有内容时优先于脚本)。</summary>
|
||||||
|
public string Blocks { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>启动热键(如 F8),空表示不绑定。</summary>
|
||||||
|
public string Hotkey { get; set; } = "";
|
||||||
|
|
||||||
|
public string Source { get; set; } = "";
|
||||||
|
|
||||||
|
public string ModeText => Infinite ? "无限循环" : $"{LoopCount} 次";
|
||||||
|
|
||||||
|
public override string ToString() => Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>任务列表 JSON 读写(.ajt)。</summary>
|
||||||
|
public static class LuaTaskFile
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions Opt = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
public static string ToJson(IEnumerable<LuaTask> tasks) => JsonSerializer.Serialize(tasks, Opt);
|
||||||
|
|
||||||
|
public static List<LuaTask> FromJson(string json)
|
||||||
|
=> JsonSerializer.Deserialize<List<LuaTask>>(json) ?? new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
using MoonSharp.Interpreter;
|
||||||
|
|
||||||
|
namespace anjianjingling.Lua
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 把自动化能力注册进 Lua 全局环境。
|
||||||
|
/// 两套写法并存:
|
||||||
|
/// 1) 全局简写(按键精灵风格) moveTo(100,200) leftClick() delay(500)
|
||||||
|
/// 2) 命名空间写法 mouse.moveTo(100,200) mouse.click() key.press("ENTER")
|
||||||
|
/// </summary>
|
||||||
|
internal static class ScriptApi
|
||||||
|
{
|
||||||
|
private const double DefaultSim = 0.85;
|
||||||
|
private const int DefaultTolerance = 10;
|
||||||
|
|
||||||
|
public static void Bind(Script script, LuaEngine engine, ILuaHost host)
|
||||||
|
{
|
||||||
|
var g = script.Globals;
|
||||||
|
|
||||||
|
// ===================== 基础 =====================
|
||||||
|
g["log"] = (Action<string>)(m => host.Log(m));
|
||||||
|
g["delay"] = (Action<double>)(ms => engine.Sleep(ms));
|
||||||
|
g["sleep"] = (Action<double>)(ms => engine.Sleep(ms));
|
||||||
|
g["delayRandom"] = (Action<double, double>)((a, b) =>
|
||||||
|
{
|
||||||
|
int lo = (int)Math.Min(a, b), hi = (int)Math.Max(a, b);
|
||||||
|
engine.Sleep(lo >= hi ? lo : Random.Shared.Next(lo, hi + 1));
|
||||||
|
});
|
||||||
|
g["random"] = (Func<double, double, double>)((a, b) =>
|
||||||
|
{
|
||||||
|
int lo = (int)Math.Min(a, b), hi = (int)Math.Max(a, b);
|
||||||
|
return lo >= hi ? lo : Random.Shared.Next(lo, hi + 1);
|
||||||
|
});
|
||||||
|
g["now"] = (Func<double>)(() => DateTimeOffset.Now.ToUnixTimeMilliseconds());
|
||||||
|
g["stop"] = (Action)(() => throw new AbortSignal(false));
|
||||||
|
|
||||||
|
// ===================== 鼠标 =====================
|
||||||
|
var mouse = new Table(script);
|
||||||
|
mouse["moveTo"] = (Action<int, int>)((x, y) => { engine.Tick(); host.MouseMove(x, y); });
|
||||||
|
mouse["moveR"] = (Action<int, int>)((dx, dy) => { engine.Tick(); host.MouseMoveR(dx, dy); });
|
||||||
|
mouse["down"] = (Action<string>)(b => { engine.Tick(); host.MouseDown(NormBtn(b)); });
|
||||||
|
mouse["up"] = (Action<string>)(b => { engine.Tick(); host.MouseUp(NormBtn(b)); });
|
||||||
|
mouse["wheel"] = (Action<int>)(d => { engine.Tick(); host.MouseWheel(d); });
|
||||||
|
mouse["drag"] = (Action<int, int, int, int, int>)((x1, y1, x2, y2, ms) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
host.MouseDrag(x1, y1, x2, y2, ms <= 0 ? 400 : ms);
|
||||||
|
});
|
||||||
|
mouse["pos"] = (Func<DynValue>)(() =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
var p = host.MousePosition();
|
||||||
|
return DynValue.NewTuple(DynValue.NewNumber(p.X), DynValue.NewNumber(p.Y));
|
||||||
|
});
|
||||||
|
|
||||||
|
// click 支持多种参数形态
|
||||||
|
mouse["click"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
string btn = "left";
|
||||||
|
int? x = null, y = null;
|
||||||
|
if (a.Count >= 3 && a[0].Type == DataType.Number)
|
||||||
|
{
|
||||||
|
x = (int)Math.Round(a[0].Number);
|
||||||
|
y = (int)Math.Round(a[1].Number);
|
||||||
|
btn = a[2].Type == DataType.String ? a[2].String : "left";
|
||||||
|
}
|
||||||
|
else if (a.Count >= 2 && a[0].Type == DataType.Number)
|
||||||
|
{
|
||||||
|
x = (int)Math.Round(a[0].Number);
|
||||||
|
y = (int)Math.Round(a[1].Number);
|
||||||
|
}
|
||||||
|
else if (a.Count >= 1 && a[0].Type == DataType.String)
|
||||||
|
{
|
||||||
|
btn = a[0].String;
|
||||||
|
}
|
||||||
|
host.MouseClick(NormBtn(btn), x, y);
|
||||||
|
return DynValue.Nil;
|
||||||
|
});
|
||||||
|
|
||||||
|
mouse["doubleClick"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
int? x = a.Count >= 2 && a[0].Type == DataType.Number ? (int)Math.Round(a[0].Number) : null;
|
||||||
|
int? y = a.Count >= 2 && a[0].Type == DataType.Number ? (int)Math.Round(a[1].Number) : null;
|
||||||
|
host.MouseDoubleClick(x, y);
|
||||||
|
return DynValue.Nil;
|
||||||
|
});
|
||||||
|
g["mouse"] = mouse;
|
||||||
|
|
||||||
|
// ===================== 键盘 =====================
|
||||||
|
var key = new Table(script);
|
||||||
|
key["press"] = (Action<string>)(k => { engine.Tick(); host.KeyPress(k); });
|
||||||
|
key["down"] = (Action<string>)(k => { engine.Tick(); host.KeyDown(k); });
|
||||||
|
key["up"] = (Action<string>)(k => { engine.Tick(); host.KeyUp(k); });
|
||||||
|
key["hotkey"] = (Action<string>)(k => { engine.Tick(); host.HotKey(k); });
|
||||||
|
key["type"] = (Action<string>)(t => { engine.Tick(); host.TypeText(t); });
|
||||||
|
g["key"] = key;
|
||||||
|
|
||||||
|
// ===================== 屏幕 / 视觉 =====================
|
||||||
|
var screen = new Table(script);
|
||||||
|
screen["size"] = (Func<DynValue>)(() =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
var (w, h) = host.ScreenSize();
|
||||||
|
return DynValue.NewTuple(DynValue.NewNumber(w), DynValue.NewNumber(h));
|
||||||
|
});
|
||||||
|
screen["color"] = (Func<int, int, string>)((x, y) => { engine.Tick(); return host.ColorAt(x, y); });
|
||||||
|
screen["shot"] = (Action<string>)(p => { engine.Tick(); host.ScreenShot(p); });
|
||||||
|
screen["pick"] = (Func<string, string>)(p =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
return host.PickRegion(string.IsNullOrWhiteSpace(p) ? null : p) ?? "";
|
||||||
|
});
|
||||||
|
|
||||||
|
screen["findColor"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
string hex = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "FF0000";
|
||||||
|
int tol = a.Count > 1 && a[1].Type == DataType.Number ? (int)Math.Round(a[1].Number) : DefaultTolerance;
|
||||||
|
var (w, h) = host.ScreenSize();
|
||||||
|
int x1 = a.Count > 2 ? (int)Math.Round(a[2].Number) : 0;
|
||||||
|
int y1 = a.Count > 3 ? (int)Math.Round(a[3].Number) : 0;
|
||||||
|
int x2 = a.Count > 4 ? (int)Math.Round(a[4].Number) : w - 1;
|
||||||
|
int y2 = a.Count > 5 ? (int)Math.Round(a[5].Number) : h - 1;
|
||||||
|
var p = host.FindColor(x1, y1, x2, y2, hex, tol);
|
||||||
|
return DynValue.NewTuple(DynValue.NewNumber(p.X), DynValue.NewNumber(p.Y));
|
||||||
|
});
|
||||||
|
|
||||||
|
screen["findImage"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
|
||||||
|
double sim = a.Count > 1 && a[1].Type == DataType.Number ? a[1].Number : DefaultSim;
|
||||||
|
var (w, h) = host.ScreenSize();
|
||||||
|
int x1 = a.Count > 2 ? (int)Math.Round(a[2].Number) : 0;
|
||||||
|
int y1 = a.Count > 3 ? (int)Math.Round(a[3].Number) : 0;
|
||||||
|
int x2 = a.Count > 4 ? (int)Math.Round(a[4].Number) : w - 1;
|
||||||
|
int y2 = a.Count > 5 ? (int)Math.Round(a[5].Number) : h - 1;
|
||||||
|
var r = host.FindImage(x1, y1, x2, y2, path, sim);
|
||||||
|
return DynValue.NewTuple(
|
||||||
|
DynValue.NewNumber(r.X), DynValue.NewNumber(r.Y), DynValue.NewNumber(r.Score));
|
||||||
|
});
|
||||||
|
|
||||||
|
screen["waitImage"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
|
||||||
|
int timeout = a.Count > 1 && a[1].Type == DataType.Number ? (int)Math.Round(a[1].Number) : 10000;
|
||||||
|
double sim = a.Count > 2 && a[2].Type == DataType.Number ? a[2].Number : DefaultSim;
|
||||||
|
|
||||||
|
var (w, h) = host.ScreenSize();
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
int rx = -1, ry = -1;
|
||||||
|
double best = 0;
|
||||||
|
while (sw.ElapsedMilliseconds < timeout)
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
var r = host.FindImage(0, 0, w - 1, h - 1, path, sim);
|
||||||
|
best = r.Score;
|
||||||
|
if (r.X >= 0) { rx = r.X; ry = r.Y; break; }
|
||||||
|
engine.Sleep(300);
|
||||||
|
}
|
||||||
|
host.Log(rx >= 0
|
||||||
|
? $"等待图片出现:找到 ({rx},{ry}) 相似度 {best:0.00}"
|
||||||
|
: $"等待图片超时:{path}(最高 {best:0.00})");
|
||||||
|
return DynValue.NewTuple(
|
||||||
|
DynValue.NewNumber(rx), DynValue.NewNumber(ry), DynValue.NewNumber(best));
|
||||||
|
});
|
||||||
|
|
||||||
|
screen["exists"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
|
||||||
|
double sim = a.Count > 1 && a[1].Type == DataType.Number ? a[1].Number : DefaultSim;
|
||||||
|
var (w, h) = host.ScreenSize();
|
||||||
|
var r = host.FindImage(0, 0, w - 1, h - 1, path, sim);
|
||||||
|
return DynValue.NewBoolean(r.X >= 0);
|
||||||
|
});
|
||||||
|
g["screen"] = screen;
|
||||||
|
|
||||||
|
// ===================== 窗口 / 系统 =====================
|
||||||
|
var win = new Table(script);
|
||||||
|
win["launch"] = (Action<string, string>)((p, args) => { engine.Tick(); host.Launch(p, args); });
|
||||||
|
win["activate"] = (Func<string, bool>)(t => { engine.Tick(); return host.ActivateWindow(t); });
|
||||||
|
win["list"] = (Func<DynValue>)(() =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
var list = new Table(script);
|
||||||
|
var arr = host.ListWindows();
|
||||||
|
for (int i = 0; i < arr.Length; i++) list[i + 1] = arr[i];
|
||||||
|
return DynValue.NewTable(list);
|
||||||
|
});
|
||||||
|
g["win"] = win;
|
||||||
|
|
||||||
|
var sys = new Table(script);
|
||||||
|
sys["time"] = (Func<double>)(() => DateTimeOffset.Now.ToUnixTimeMilliseconds());
|
||||||
|
sys["sleep"] = (Action<double>)(ms => engine.Sleep(ms));
|
||||||
|
sys["speed"] = (Func<double, double>)(s => { engine.Speed = s <= 0 ? 1 : s; return engine.Speed; });
|
||||||
|
g["sys"] = sys;
|
||||||
|
|
||||||
|
// ===================== 剪贴板 =====================
|
||||||
|
var clip = new Table(script);
|
||||||
|
clip["get"] = (Func<string>)(() => host.ClipGet());
|
||||||
|
clip["set"] = (Action<string>)(t => host.ClipSet(t));
|
||||||
|
g["clip"] = clip;
|
||||||
|
|
||||||
|
// ===================== 浏览器 =====================
|
||||||
|
var browser = new Table(script);
|
||||||
|
browser["open"] = (Action<string>)(u => { engine.Tick(); host.BrowserOpen(u); });
|
||||||
|
browser["click"] = (Func<string, bool>)(s => { engine.Tick(); return host.BrowserClick(s); });
|
||||||
|
browser["type"] = (Func<string, string, bool>)((s, t) => { engine.Tick(); return host.BrowserType(s, t); });
|
||||||
|
browser["wait"] = (Func<string, int, bool>)((s, ms) => { engine.Tick(); return host.BrowserWaitFor(s, ms <= 0 ? 10000 : ms); });
|
||||||
|
browser["text"] = (Func<string, string>)(s => { engine.Tick(); return host.BrowserGetText(s); });
|
||||||
|
browser["js"] = (Func<string, string>)(js => { engine.Tick(); return host.BrowserExecJs(js); });
|
||||||
|
browser["shot"] = (Action<string>)(p => { engine.Tick(); host.BrowserShot(p); });
|
||||||
|
browser["back"] = (Action)(() => { engine.Tick(); host.BrowserBack(); });
|
||||||
|
browser["forward"] = (Action)(() => { engine.Tick(); host.BrowserForward(); });
|
||||||
|
browser["reload"] = (Action)(() => { engine.Tick(); host.BrowserReload(); });
|
||||||
|
g["browser"] = browser;
|
||||||
|
|
||||||
|
// ===================== 全局简写 =====================
|
||||||
|
g["moveTo"] = mouse["moveTo"];
|
||||||
|
g["moveR"] = mouse["moveR"];
|
||||||
|
g["click"] = mouse["click"];
|
||||||
|
g["leftClick"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
|
||||||
|
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
|
||||||
|
host.MouseClick("left", x, y);
|
||||||
|
return DynValue.Nil;
|
||||||
|
});
|
||||||
|
g["rightClick"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
|
||||||
|
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
|
||||||
|
host.MouseClick("right", x, y);
|
||||||
|
return DynValue.Nil;
|
||||||
|
});
|
||||||
|
g["middleClick"] = DynValue.NewCallback((ctx, a) =>
|
||||||
|
{
|
||||||
|
engine.Tick();
|
||||||
|
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
|
||||||
|
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
|
||||||
|
host.MouseClick("middle", x, y);
|
||||||
|
return DynValue.Nil;
|
||||||
|
});
|
||||||
|
g["doubleClick"] = mouse["doubleClick"];
|
||||||
|
g["wheel"] = mouse["wheel"];
|
||||||
|
g["drag"] = mouse["drag"];
|
||||||
|
|
||||||
|
g["keyPress"] = key["press"];
|
||||||
|
g["keyDown"] = key["down"];
|
||||||
|
g["keyUp"] = key["up"];
|
||||||
|
g["hotkey"] = key["hotkey"];
|
||||||
|
g["typeText"] = key["type"];
|
||||||
|
g["sayText"] = key["type"];
|
||||||
|
|
||||||
|
g["findImage"] = screen["findImage"];
|
||||||
|
g["findColor"] = screen["findColor"];
|
||||||
|
g["waitImage"] = screen["waitImage"];
|
||||||
|
g["exists"] = screen["exists"];
|
||||||
|
g["colorAt"] = screen["color"];
|
||||||
|
g["screenshot"] = screen["shot"];
|
||||||
|
|
||||||
|
g["run"] = win["launch"];
|
||||||
|
g["activate"] = win["activate"];
|
||||||
|
g["clipGet"] = clip["get"];
|
||||||
|
g["clipSet"] = clip["set"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormBtn(string b) => b.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"右" or "right" or "r" or "rightclick" => "right",
|
||||||
|
"中" or "middle" or "m" => "middle",
|
||||||
|
_ => "left"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace anjianjingling.Native
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Win32 API 统一封装:SendInput、窗口、热键、消息。
|
||||||
|
/// </summary>
|
||||||
|
internal static class Win32
|
||||||
|
{
|
||||||
|
#region SendInput 结构与常量
|
||||||
|
|
||||||
|
public const uint INPUT_MOUSE = 0;
|
||||||
|
public const uint INPUT_KEYBOARD = 1;
|
||||||
|
|
||||||
|
public const uint MOUSEEVENTF_MOVE = 0x0001;
|
||||||
|
public const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
|
||||||
|
public const uint MOUSEEVENTF_LEFTUP = 0x0004;
|
||||||
|
public const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
|
||||||
|
public const uint MOUSEEVENTF_RIGHTUP = 0x0010;
|
||||||
|
public const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020;
|
||||||
|
public const uint MOUSEEVENTF_MIDDLEUP = 0x0040;
|
||||||
|
public const uint MOUSEEVENTF_WHEEL = 0x0800;
|
||||||
|
public const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
|
||||||
|
public const uint MOUSEEVENTF_VIRTUALDESK = 0x4000;
|
||||||
|
public const int WHEEL_DELTA = 120;
|
||||||
|
|
||||||
|
public const uint KEYEVENTF_EXTENDEDKEY = 0x0001;
|
||||||
|
public const uint KEYEVENTF_KEYUP = 0x0002;
|
||||||
|
public const uint KEYEVENTF_UNICODE = 0x0004;
|
||||||
|
public const uint KEYEVENTF_SCANCODE = 0x0008;
|
||||||
|
|
||||||
|
public const int SM_CXVIRTUALSCREEN = 78;
|
||||||
|
public const int SM_CYVIRTUALSCREEN = 79;
|
||||||
|
public const int SM_XVIRTUALSCREEN = 76;
|
||||||
|
public const int SM_YVIRTUALSCREEN = 77;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct MOUSEINPUT
|
||||||
|
{
|
||||||
|
public int dx;
|
||||||
|
public int dy;
|
||||||
|
public uint mouseData;
|
||||||
|
public uint dwFlags;
|
||||||
|
public uint time;
|
||||||
|
public IntPtr dwExtraInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct KEYBDINPUT
|
||||||
|
{
|
||||||
|
public ushort wVk;
|
||||||
|
public ushort wScan;
|
||||||
|
public uint dwFlags;
|
||||||
|
public uint time;
|
||||||
|
public IntPtr dwExtraInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
public struct INPUTUNION
|
||||||
|
{
|
||||||
|
[FieldOffset(0)] public MOUSEINPUT mi;
|
||||||
|
[FieldOffset(0)] public KEYBDINPUT ki;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct INPUT
|
||||||
|
{
|
||||||
|
public uint type;
|
||||||
|
public INPUTUNION u;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool GetCursorPos(out Point lpPoint);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool SetCursorPos(int x, int y);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern int GetSystemMetrics(int nIndex);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern uint MapVirtualKey(uint uCode, uint uMapType);
|
||||||
|
public const uint MAPVK_VK_TO_VSC = 0;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 热键
|
||||||
|
|
||||||
|
public const int WM_HOTKEY = 0x0312;
|
||||||
|
public const uint MOD_ALT = 0x0001;
|
||||||
|
public const uint MOD_CONTROL = 0x0002;
|
||||||
|
public const uint MOD_SHIFT = 0x0004;
|
||||||
|
public const uint MOD_NOREPEAT = 0x4000;
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 窗口
|
||||||
|
|
||||||
|
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern int GetWindowTextLength(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool IsIconic(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern uint GetCurrentThreadId();
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern IntPtr FindWindow(string? lpClassName, string lpWindowName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
|
||||||
|
public const uint PW_RENDERFULLCONTENT = 2;
|
||||||
|
|
||||||
|
public const int SW_RESTORE = 9;
|
||||||
|
public const int SW_SHOW = 5;
|
||||||
|
|
||||||
|
// 窗口消息
|
||||||
|
public const uint WM_LBUTTONDOWN = 0x0201;
|
||||||
|
public const uint WM_LBUTTONUP = 0x0202;
|
||||||
|
public const uint WM_RBUTTONDOWN = 0x0204;
|
||||||
|
public const uint WM_RBUTTONUP = 0x0205;
|
||||||
|
public const uint WM_KEYDOWN = 0x0100;
|
||||||
|
public const uint WM_KEYUP = 0x0101;
|
||||||
|
public const uint WM_CHAR = 0x0102;
|
||||||
|
public const IntPtr MK_LBUTTON = 0x0001;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct RECT
|
||||||
|
{
|
||||||
|
public int Left, Top, Right, Bottom;
|
||||||
|
public int Width => Right - Left;
|
||||||
|
public int Height => Bottom - Top;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IntPtr MakeLParam(int low, int high)
|
||||||
|
=> (IntPtr)((high << 16) | (low & 0xFFFF));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
using anjianjingling.Forms;
|
||||||
|
|
||||||
|
namespace anjianjingling
|
||||||
|
{
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 程序入口。
|
||||||
|
/// </summary>
|
||||||
|
[STAThread]
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
ApplicationConfiguration.Initialize();
|
||||||
|
Application.Run(new MainForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- 示例 1:无限找图点击
|
||||||
|
-- 用法:把要点击的按钮截图(F11 框选截图)保存到 images/start.png,再运行本脚本
|
||||||
|
-- 热键:F8 启动队列 / F9 暂停 / F10 停止;鼠标甩到左上角 (0,0) 紧急停止
|
||||||
|
|
||||||
|
log("开始找图循环")
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local x, y = findImage("images/start.png", 0.85)
|
||||||
|
|
||||||
|
if x >= 0 then
|
||||||
|
moveTo(x, y)
|
||||||
|
delay(80)
|
||||||
|
leftClick()
|
||||||
|
log("已点击 (" .. x .. "," .. y .. ")")
|
||||||
|
delayRandom(600, 1200)
|
||||||
|
else
|
||||||
|
log("未找到图片,1 秒后重试")
|
||||||
|
delay(1000)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- 示例 3:录制脚本的典型形态(由 F12 录制自动生成,可手工改写)
|
||||||
|
-- 说明:录制只会记录「按键 / 点击 / 滚动 / 拖动」,界面元素识别需自己补 findImage
|
||||||
|
|
||||||
|
-- 点击浏览器地址栏并输入网址
|
||||||
|
moveTo(742, 46)
|
||||||
|
leftClick(742, 46)
|
||||||
|
delay(120)
|
||||||
|
hotkey("Ctrl+A")
|
||||||
|
typeText("www.example.com")
|
||||||
|
keyPress("ENTER")
|
||||||
|
delay(2500)
|
||||||
|
|
||||||
|
-- 点击页面上的按钮(坐标来自录制,换机器或换分辨率需重录)
|
||||||
|
moveTo(512, 388)
|
||||||
|
leftClick(512, 388)
|
||||||
|
delay(800)
|
||||||
|
|
||||||
|
-- 滚动查看
|
||||||
|
wheel(-3)
|
||||||
|
delay(500)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- 示例 2:打开记事本并输入中文
|
||||||
|
-- 演示键盘输入(Unicode 注入,中文不受输入法影响)、组合键、循环
|
||||||
|
|
||||||
|
delay(500)
|
||||||
|
run("notepad.exe")
|
||||||
|
delay(1500)
|
||||||
|
activate("记事本")
|
||||||
|
|
||||||
|
typeText("按键精灵 · Lua 脚本自动输入")
|
||||||
|
keyPress("ENTER")
|
||||||
|
delay(200)
|
||||||
|
|
||||||
|
for i = 1, 5 do
|
||||||
|
typeText("第 " .. i .. " 行")
|
||||||
|
keyPress("ENTER")
|
||||||
|
delay(150)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 全选复制,读回剪贴板验证
|
||||||
|
hotkey("Ctrl+A")
|
||||||
|
delay(100)
|
||||||
|
hotkey("Ctrl+C")
|
||||||
|
delay(200)
|
||||||
|
local clip = clipGet()
|
||||||
|
log("剪贴板内容长度:" .. #clip)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace anjianjingling.Vision
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 找色:LockBits 锁内存逐像素扫描,支持容差。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ColorFinder
|
||||||
|
{
|
||||||
|
/// <summary>解析 "RRGGBB" 或 "#RRGGBB" 颜色。</summary>
|
||||||
|
public static Color ParseHex(string hex)
|
||||||
|
{
|
||||||
|
hex = hex.Trim().TrimStart('#');
|
||||||
|
if (hex.Length != 6) throw new ArgumentException($"颜色格式非法:{hex}(应为 RRGGBB)");
|
||||||
|
int r = Convert.ToInt32(hex[0..2], 16);
|
||||||
|
int g = Convert.ToInt32(hex[2..4], 16);
|
||||||
|
int b = Convert.ToInt32(hex[4..6], 16);
|
||||||
|
return Color.FromArgb(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在 bmp 的指定区域内找颜色,返回第一个匹配点的 bmp 内坐标;未找到返回 (-1,-1)。
|
||||||
|
/// </summary>
|
||||||
|
public Point FindColor(Bitmap bmp, int x1, int y1, int x2, int y2, Color target, int tolerance = 0)
|
||||||
|
{
|
||||||
|
// 裁剪到合法范围
|
||||||
|
x1 = Math.Clamp(x1, 0, bmp.Width - 1);
|
||||||
|
x2 = Math.Clamp(x2, 0, bmp.Width - 1);
|
||||||
|
y1 = Math.Clamp(y1, 0, bmp.Height - 1);
|
||||||
|
y2 = Math.Clamp(y2, 0, bmp.Height - 1);
|
||||||
|
if (x1 > x2 || y1 > y2) return new Point(-1, -1);
|
||||||
|
|
||||||
|
var rect = new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
|
||||||
|
using var fast = new LockedBitmap(bmp, rect);
|
||||||
|
for (int y = 0; y < rect.Height; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < rect.Width; x++)
|
||||||
|
{
|
||||||
|
var c = fast.GetPixel(x, y);
|
||||||
|
if (Math.Abs(c.R - target.R) <= tolerance &&
|
||||||
|
Math.Abs(c.G - target.G) <= tolerance &&
|
||||||
|
Math.Abs(c.B - target.B) <= tolerance)
|
||||||
|
{
|
||||||
|
return new Point(rect.X + x, rect.Y + y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Point(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>LockBits 包装,按 Format24bpp/32bpp 通用读取 BGR。</summary>
|
||||||
|
private sealed class LockedBitmap : IDisposable
|
||||||
|
{
|
||||||
|
private readonly BitmapData _data;
|
||||||
|
private readonly Bitmap _bmp;
|
||||||
|
private readonly int _stride;
|
||||||
|
private readonly byte[] _buf;
|
||||||
|
private readonly int _bytesPerPixel;
|
||||||
|
|
||||||
|
public LockedBitmap(Bitmap bmp, Rectangle rect)
|
||||||
|
{
|
||||||
|
_bmp = bmp;
|
||||||
|
_bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
|
||||||
|
if (_bytesPerPixel < 3) _bytesPerPixel = 3;
|
||||||
|
_data = bmp.LockBits(rect, ImageLockMode.ReadOnly,
|
||||||
|
_bytesPerPixel == 3 ? PixelFormat.Format24bppRgb : PixelFormat.Format32bppArgb);
|
||||||
|
_stride = Math.Abs(_data.Stride);
|
||||||
|
_buf = new byte[_stride * rect.Height];
|
||||||
|
Marshal.Copy(_data.Scan0, _buf, 0, _buf.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color GetPixel(int x, int y)
|
||||||
|
{
|
||||||
|
int i = y * _stride + x * _bytesPerPixel;
|
||||||
|
return Color.FromArgb(_buf[i + 2], _buf[i + 1], _buf[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_bmp.UnlockBits(_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using OpenCvSharp;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
|
||||||
|
namespace anjianjingling.Vision
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 找图:OpenCV 模板匹配(TM_CCOEFF_NORMED),支持相似度阈值。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ImageFinder : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Mat> _templateCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static Mat BitmapToMat(Bitmap bmp)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
bmp.Save(ms, ImageFormat.Png);
|
||||||
|
return Cv2.ImDecode(ms.ToArray(), ImreadModes.Color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mat LoadTemplate(string path)
|
||||||
|
{
|
||||||
|
if (_templateCache.TryGetValue(path, out var cached)) return cached;
|
||||||
|
if (!File.Exists(path))
|
||||||
|
throw new FileNotFoundException($"模板图片不存在:{path}");
|
||||||
|
var mat = Cv2.ImRead(path, ImreadModes.Color);
|
||||||
|
if (mat.Empty())
|
||||||
|
throw new InvalidDataException($"模板图片无法读取:{path}");
|
||||||
|
_templateCache[path] = mat;
|
||||||
|
return mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在 haystack 位图中查找模板。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="haystack">被查找的画面(通常是区域截图)</param>
|
||||||
|
/// <param name="templatePath">模板图路径</param>
|
||||||
|
/// <param name="threshold">相似度阈值 0~1</param>
|
||||||
|
/// <param name="offsetX">结果坐标要加上的屏幕偏移 X</param>
|
||||||
|
/// <param name="offsetY">结果坐标要加上的屏幕偏移 Y</param>
|
||||||
|
/// <returns>匹配中心屏幕坐标与相似度;未达阈值返回 (-1,-1,score)</returns>
|
||||||
|
public (int X, int Y, double Score) Find(Bitmap haystack, string templatePath,
|
||||||
|
double threshold, int offsetX = 0, int offsetY = 0)
|
||||||
|
{
|
||||||
|
using var hay = BitmapToMat(haystack);
|
||||||
|
using var tpl = LoadTemplate(templatePath);
|
||||||
|
|
||||||
|
if (tpl.Width > hay.Width || tpl.Height > hay.Height)
|
||||||
|
throw new ArgumentException($"模板图({tpl.Width}x{tpl.Height})大于查找区域({hay.Width}x{hay.Height})");
|
||||||
|
|
||||||
|
using var result = new Mat();
|
||||||
|
Cv2.MatchTemplate(hay, tpl, result, TemplateMatchModes.CCoeffNormed);
|
||||||
|
Cv2.MinMaxLoc(result, out _, out double maxVal, out _, out OpenCvSharp.Point maxLoc);
|
||||||
|
|
||||||
|
if (maxVal >= threshold)
|
||||||
|
{
|
||||||
|
int cx = offsetX + maxLoc.X + tpl.Width / 2;
|
||||||
|
int cy = offsetY + maxLoc.Y + tpl.Height / 2;
|
||||||
|
return (cx, cy, maxVal);
|
||||||
|
}
|
||||||
|
return (-1, -1, maxVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>清除模板缓存(模板文件更新后调用)。</summary>
|
||||||
|
public void ClearCache()
|
||||||
|
{
|
||||||
|
foreach (var m in _templateCache.Values) m.Dispose();
|
||||||
|
_templateCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ClearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using anjianjingling.Native;
|
||||||
|
|
||||||
|
namespace anjianjingling.Vision
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 屏幕/区域/窗口截图(GDI BitBlt)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ScreenCapture
|
||||||
|
{
|
||||||
|
/// <summary>截取整个虚拟屏幕(所有显示器)。</summary>
|
||||||
|
public Bitmap CaptureScreen()
|
||||||
|
{
|
||||||
|
int x = Win32.GetSystemMetrics(Win32.SM_XVIRTUALSCREEN);
|
||||||
|
int y = Win32.GetSystemMetrics(Win32.SM_YVIRTUALSCREEN);
|
||||||
|
int w = Win32.GetSystemMetrics(Win32.SM_CXVIRTUALSCREEN);
|
||||||
|
int h = Win32.GetSystemMetrics(Win32.SM_CYVIRTUALSCREEN);
|
||||||
|
return CaptureRegion(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>截取指定屏幕区域。</summary>
|
||||||
|
public Bitmap CaptureRegion(int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
if (width <= 0 || height <= 0)
|
||||||
|
throw new ArgumentException($"截图区域非法:({x},{y},{width},{height})");
|
||||||
|
|
||||||
|
var bmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
|
||||||
|
using (var g = Graphics.FromImage(bmp))
|
||||||
|
{
|
||||||
|
g.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 截取指定窗口(含被遮挡部分,依赖 PrintWindow;部分程序不支持则返回 null)。
|
||||||
|
/// </summary>
|
||||||
|
public Bitmap? CaptureWindow(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
if (!Win32.GetClientRect(hWnd, out var rc) || rc.Width <= 0 || rc.Height <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var bmp = new Bitmap(rc.Width, rc.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
|
||||||
|
using (var g = Graphics.FromImage(bmp))
|
||||||
|
{
|
||||||
|
IntPtr hdc = g.GetHdc();
|
||||||
|
bool ok = Win32.PrintWindow(hWnd, hdc, Win32.PW_RENDERFULLCONTENT);
|
||||||
|
g.ReleaseHdc(hdc);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
bmp.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>截图并保存到文件。</summary>
|
||||||
|
public string SaveRegion(int x, int y, int width, int height, string path)
|
||||||
|
{
|
||||||
|
using var bmp = CaptureRegion(x, y, width, height);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
|
||||||
|
bmp.Save(path, System.Drawing.Imaging.ImageFormat.Png);
|
||||||
|
return Path.GetFullPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using anjianjingling.Native;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace anjianjingling.Window
|
||||||
|
{
|
||||||
|
public record WindowInfo(IntPtr Handle, string Title);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统级窗口管理:枚举、查找、激活、启动程序、后台消息投递。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WindowManager
|
||||||
|
{
|
||||||
|
/// <summary>启动外部程序或打开文件。</summary>
|
||||||
|
public void Launch(string path, string? arguments = null)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = path,
|
||||||
|
UseShellExecute = true
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(arguments)) psi.Arguments = arguments;
|
||||||
|
Process.Start(psi);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>枚举所有可见且有标题的顶层窗口。</summary>
|
||||||
|
public List<WindowInfo> GetWindows()
|
||||||
|
{
|
||||||
|
var list = new List<WindowInfo>();
|
||||||
|
Win32.EnumWindows((h, _) =>
|
||||||
|
{
|
||||||
|
if (Win32.IsWindowVisible(h))
|
||||||
|
{
|
||||||
|
int len = Win32.GetWindowTextLength(h);
|
||||||
|
if (len > 0)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(len + 1);
|
||||||
|
Win32.GetWindowText(h, sb, sb.Capacity);
|
||||||
|
var title = sb.ToString().Trim();
|
||||||
|
if (title.Length > 0) list.Add(new WindowInfo(h, title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, IntPtr.Zero);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按标题关键字(包含匹配,不区分大小写)查找窗口。</summary>
|
||||||
|
public IntPtr FindByTitle(string keyword)
|
||||||
|
{
|
||||||
|
foreach (var w in GetWindows())
|
||||||
|
{
|
||||||
|
if (w.Title.Contains(keyword, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return w.Handle;
|
||||||
|
}
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>激活窗口并前置(最小化则恢复,使用 AttachThreadInput 解决前台锁定)。</summary>
|
||||||
|
public bool Activate(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
if (hWnd == IntPtr.Zero) return false;
|
||||||
|
if (Win32.IsIconic(hWnd)) Win32.ShowWindow(hWnd, Win32.SW_RESTORE);
|
||||||
|
|
||||||
|
uint foregroundThread = Win32.GetWindowThreadProcessId(Win32.GetForegroundWindow(), out _);
|
||||||
|
uint currentThread = Win32.GetCurrentThreadId();
|
||||||
|
|
||||||
|
if (foregroundThread != currentThread)
|
||||||
|
{
|
||||||
|
Win32.AttachThreadInput(currentThread, foregroundThread, true);
|
||||||
|
Win32.SetForegroundWindow(hWnd);
|
||||||
|
Win32.AttachThreadInput(currentThread, foregroundThread, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Win32.SetForegroundWindow(hWnd);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按标题激活。</summary>
|
||||||
|
public bool ActivateByTitle(string keyword)
|
||||||
|
{
|
||||||
|
var h = FindByTitle(keyword);
|
||||||
|
return h != IntPtr.Zero && Activate(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取窗口屏幕矩形。</summary>
|
||||||
|
public Rectangle? GetBounds(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
if (Win32.GetWindowRect(hWnd, out var r))
|
||||||
|
return new Rectangle(r.Left, r.Top, r.Width, r.Height);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 后台点击:向窗口句柄 PostMessage 发送鼠标按下/弹起(客户区坐标),不抢焦点。
|
||||||
|
/// 注意:对 DirectX/自绘程序可能无效。
|
||||||
|
/// </summary>
|
||||||
|
public void PostClick(IntPtr hWnd, int clientX, int clientY, bool right = false)
|
||||||
|
{
|
||||||
|
var lp = Win32.MakeLParam(clientX, clientY);
|
||||||
|
uint down = right ? Win32.WM_RBUTTONDOWN : Win32.WM_LBUTTONDOWN;
|
||||||
|
uint up = right ? Win32.WM_RBUTTONUP : Win32.WM_LBUTTONUP;
|
||||||
|
Win32.PostMessage(hWnd, down, Win32.MK_LBUTTON, lp);
|
||||||
|
Thread.Sleep(60);
|
||||||
|
Win32.PostMessage(hWnd, up, IntPtr.Zero, lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<AssemblyName>anjianjingling</AssemblyName>
|
||||||
|
<RootNamespace>anjianjingling</RootNamespace>
|
||||||
|
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
|
||||||
|
<NoWarn>$(NoWarn);NU1701</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MoonSharp" Version="2.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3351.48" />
|
||||||
|
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20240616" />
|
||||||
|
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20240616" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="assets\**\*">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Samples\**\*">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Forms\BlockCanvas.cs">
|
||||||
|
<SubType>Component</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Forms\LuaEditor.cs">
|
||||||
|
<SubType>UserControl</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Forms\MainForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Forms\OverlayForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Forms\PickerForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="anjianjingling.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="anjianjingling.app" />
|
||||||
|
|
||||||
|
<!-- 提权:系统级控制管理员目标程序所必需 -->
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<!-- DPI 感知由项目属性 ApplicationHighDpiMode=PerMonitorV2 统一配置 -->
|
||||||
|
|
||||||
|
<!-- 兼容性:Windows 10/11 -->
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -0,0 +1,510 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<doc>
|
||||||
|
<assembly>
|
||||||
|
<name>Microsoft.Web.WebView2.WinForms</name>
|
||||||
|
</assembly>
|
||||||
|
<members>
|
||||||
|
<member name="T:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties">
|
||||||
|
<summary>
|
||||||
|
This class is a bundle of the most common parameters used to create <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> and <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/> instances.
|
||||||
|
Its main purpose is to be set to <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> in order to customize the environment and/or controller used by a <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/> during implicit initialization.
|
||||||
|
</summary>
|
||||||
|
<remarks>
|
||||||
|
This class isn't intended to contain all possible environment or controller customization options.
|
||||||
|
If you need complete control over the environment and/or controller used by a WebView2 control then you'll need to initialize the control explicitly by
|
||||||
|
creating your own environment (with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/>) and/or controller options (with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateCoreWebView2ControllerOptions"/>) and passing them to <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/>
|
||||||
|
*before* you set the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property to anything.
|
||||||
|
See the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/> class documentation for an initialization overview.
|
||||||
|
</remarks>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.#ctor">
|
||||||
|
<summary>
|
||||||
|
Creates a new instance of <see cref="T:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties"/> with default data for all properties.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.BrowserExecutableFolder">
|
||||||
|
<summary>
|
||||||
|
Gets or sets the value to pass as the browserExecutableFolder parameter of <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.UserDataFolder">
|
||||||
|
<summary>
|
||||||
|
Gets or sets the value to pass as the userDataFolder parameter of <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.Language">
|
||||||
|
<summary>
|
||||||
|
Gets or sets the value to use for the Language property of the CoreWebView2EnvironmentOptions parameter passed to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.ProfileName">
|
||||||
|
<summary>
|
||||||
|
Gets or sets the value to use for the ProfileName property of the CoreWebView2ControllerOptions parameter passed to CreateCoreWebView2ControllerWithOptionsAsync when creating an controller with this instance.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.AdditionalBrowserArguments">
|
||||||
|
<summary>
|
||||||
|
Gets or sets the value to pass as the AdditionalBrowserArguments parameter of <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions"/> which is passed to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.IsInPrivateModeEnabled">
|
||||||
|
<summary>
|
||||||
|
Gets or sets the value to use for the IsInPrivateModeEnabled property of the CoreWebView2ControllerOptions parameter passed to CreateCoreWebView2ControllerWithOptionsAsync when creating an controller with this instance.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.CreateEnvironmentAsync">
|
||||||
|
<summary>
|
||||||
|
Create a <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> using the current values of this instance's properties.
|
||||||
|
</summary>
|
||||||
|
<returns>A task which will provide the created environment on completion, or null if no environment-related options are set.</returns>
|
||||||
|
<remarks>
|
||||||
|
As long as no other properties on this instance are changed, repeated calls to this method will return the same task/environment as earlier calls.
|
||||||
|
If some other property is changed then the next call to this method will return a different task/environment.
|
||||||
|
</remarks>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.CreateCoreWebView2ControllerOptions(Microsoft.Web.WebView2.Core.CoreWebView2Environment)">
|
||||||
|
<summary>
|
||||||
|
Creates a <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> using the current values of this instance's properties.
|
||||||
|
</summary>
|
||||||
|
<returns>A <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> object or null if no controller-related properties are set.</returns>
|
||||||
|
<exception cref="T:System.NullReferenceException">Thrown if the parameter environment is null.</exception>
|
||||||
|
</member>
|
||||||
|
<member name="T:Microsoft.Web.WebView2.WinForms.WebView2">
|
||||||
|
<summary>
|
||||||
|
Control to embed WebView2 in WinForms.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.#ctor">
|
||||||
|
<summary>
|
||||||
|
Create a new WebView2 WinForms control.
|
||||||
|
After construction the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property is <c>null</c>.
|
||||||
|
Call <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> to initialize the underlying <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/>.
|
||||||
|
</summary>
|
||||||
|
<remarks>
|
||||||
|
This control is effectively a wrapper around the WebView2 COM API, which you can find documentation for here: https://aka.ms/webview2
|
||||||
|
You can directly access the underlying ICoreWebView2 interface and all of its functionality by accessing the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property.
|
||||||
|
Some of the most common COM functionality is also accessible directly through wrapper methods/properties/events on the control.
|
||||||
|
|
||||||
|
Upon creation, the control's CoreWebView2 property will be null.
|
||||||
|
This is because creating the CoreWebView2 is an expensive operation which involves things like launching Edge browser processes.
|
||||||
|
There are two ways to cause the CoreWebView2 to be created:
|
||||||
|
1) Call the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> method. This is referred to as explicit initialization.
|
||||||
|
2) Set the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property. This is referred to as implicit initialization.
|
||||||
|
Either option will start initialization in the background and return back to the caller without waiting for it to finish.
|
||||||
|
To specify options regarding the initialization process, either pass your own <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> to EnsureCoreWebView2Async or set the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> property prior to initialization.
|
||||||
|
|
||||||
|
When initialization has finished (regardless of how it was triggered) then the following things will occur, in this order:
|
||||||
|
1) The control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked. If you need to perform one time setup operations on the CoreWebView2 prior to its use then you should do so in a handler for that event.
|
||||||
|
2) If a Uri has been set to the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property then the control will start navigating to it in the background (i.e. these steps will continue without waiting for the navigation to finish).
|
||||||
|
3) The Task returned from <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> will complete.
|
||||||
|
|
||||||
|
For more details about any of the methods/properties/events involved in the initialization process, see its specific documentation.
|
||||||
|
|
||||||
|
Accelerator key presses (e.g. Ctrl+P) that occur within the control will
|
||||||
|
fire standard key press events such as OnKeyDown. You can suppress the
|
||||||
|
control's default implementation of an accelerator key press (e.g.
|
||||||
|
printing, in the case of Ctrl+P) by setting the Handled property of its
|
||||||
|
EventArgs to true. Also note that the underlying browser process is
|
||||||
|
blocked while these handlers execute, so:
|
||||||
|
<list type="number">
|
||||||
|
<item>
|
||||||
|
You should avoid doing a lot of work in these handlers.
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
Some of the WebView2 and CoreWebView2 APIs may throw errors if
|
||||||
|
invoked within these handlers due to being unable to communicate with
|
||||||
|
the browser process.
|
||||||
|
</item>
|
||||||
|
</list>
|
||||||
|
If you need to do a lot of work and/or invoke WebView2 APIs in response to
|
||||||
|
accelerator keys then consider kicking off a background task or queuing
|
||||||
|
the work for later execution on the UI thread.
|
||||||
|
</remarks>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Dispose(System.Boolean)">
|
||||||
|
<summary>
|
||||||
|
Cleans up any resources being used.
|
||||||
|
</summary>
|
||||||
|
<param name="disposing"><c>true</c> if managed resources should be disposed; otherwise, <c>false</c>.</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnPaint(System.Windows.Forms.PaintEventArgs)">
|
||||||
|
<summary>
|
||||||
|
Overrides the base OnPaint event to have custom actions
|
||||||
|
in designer mode
|
||||||
|
</summary>
|
||||||
|
<param name="e">The graphics devices which is the source</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.WndProc(System.Windows.Forms.Message@)">
|
||||||
|
<summary>
|
||||||
|
Overrides the base WndProc events to handle specific window messages.
|
||||||
|
</summary>
|
||||||
|
<param name="m">The Message object containing the HWND window message and parameters</param>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties">
|
||||||
|
<summary>
|
||||||
|
Gets or sets a bag of options which are used during initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||||
|
This property cannot be modified (an exception will be thrown) after initialization of the control's CoreWebView2 has started.
|
||||||
|
</summary>
|
||||||
|
<exception cref="T:System.InvalidOperationException">Thrown if initialization of the control's CoreWebView2 has already started.</exception>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)">
|
||||||
|
<summary>
|
||||||
|
Explicitly trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||||
|
</summary>
|
||||||
|
<param name="environment">
|
||||||
|
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> that should be used to create the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||||
|
Creating your own environment gives you control over several options that affect how the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is initialized.
|
||||||
|
If you pass <c>null</c> (the default value) then a default environment will be created and used automatically.
|
||||||
|
</param>
|
||||||
|
<param name="controllerOptions">
|
||||||
|
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> that should be used to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/>.
|
||||||
|
Creating your own controller options gives you control over several options that affect how the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/> is initialized.
|
||||||
|
If you pass a controllerOptions to this method then it will override any settings specified on the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> property.
|
||||||
|
If you pass <c>null</c> (the default value) and no value has been set to <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> then a default controllerOptions will be created and used automatically.
|
||||||
|
</param>
|
||||||
|
<returns>
|
||||||
|
A Task that represents the background initialization process.
|
||||||
|
When the task completes then the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property will be available for use (i.e. non-null).
|
||||||
|
Note that the control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked before the task completes
|
||||||
|
or on exceptions.
|
||||||
|
</returns>
|
||||||
|
<remarks>
|
||||||
|
Unless previous initialization has already failed, calling this method additional times with the same parameter will have no effect (any specified environment is ignored) and return the same Task as the first call.
|
||||||
|
Unless previous initialization has already failed, calling this method after initialization has been implicitly triggered by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property will have no effect if no environment is given
|
||||||
|
and simply return a Task representing that initialization already in progress.
|
||||||
|
Unless previous initialization has already failed, calling this method with a different environment after initialization has begun will result in an <see cref="T:System.ArgumentException"/>. For example, this can happen if you begin initialization
|
||||||
|
by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property and then call this method with a new environment, if you begin initialization with <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> and then call this method with a new
|
||||||
|
environment, or if you begin initialization with one environment and then call this method with no environment specified.
|
||||||
|
When this method is called after previous initialization has failed, it will trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> again.
|
||||||
|
Note that even though this method is asynchronous and returns a Task, it still must be called on the UI thread like most public functionality of most UI controls.
|
||||||
|
<para>
|
||||||
|
The following summarizes the possible error values and a description of why these errors occur.
|
||||||
|
<list type="table">
|
||||||
|
<listheader>
|
||||||
|
<description>Error Value</description>
|
||||||
|
<description>Description</description>
|
||||||
|
</listheader>
|
||||||
|
<item>
|
||||||
|
<description><c>HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)</c></description>
|
||||||
|
<description>*\\Edge\\Application* path used in browserExecutableFolder.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>HRESULT_FROM_WIN32(ERROR_INVALID_STATE)</c></description>
|
||||||
|
<description>Specified options do not match the options of the WebViews that are currently running in the shared browser process.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>HRESULT_FROM_WIN32(ERROR_INVALID_WINDOW_HANDLE)</c></description>
|
||||||
|
<description>WebView2 Initialization failed due to an invalid host HWND parentWindow.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>HRESULT_FROM_WIN32(ERROR_DISK_FULL)</c></description>
|
||||||
|
<description>WebView2 Initialization failed due to reaching the maximum number of installed runtime versions.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>HRESULT_FROM_WIN32(ERROR_PRODUCT_UNINSTALLED</c></description>
|
||||||
|
<description>If the Webview depends upon an installed WebView2 Runtime version and it is uninstalled.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)</c></description>
|
||||||
|
<description>Could not find Edge installation.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>HRESULT_FROM_WIN32(ERROR_FILE_EXISTS)</c></description>
|
||||||
|
<description>User data folder cannot be created because a file with the same name already exists.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>E_ACCESSDENIED</c></description>
|
||||||
|
<description>Unable to create user data folder, Access Denied.</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description><c>E_FAIL</c></description>
|
||||||
|
<description>Edge runtime unable to start.</description>
|
||||||
|
</item>
|
||||||
|
</list>
|
||||||
|
</para>
|
||||||
|
</remarks>
|
||||||
|
<exception cref="T:System.ArgumentException">
|
||||||
|
Thrown if this method is called with a different environment than when it was initialized. See Remarks for more info.
|
||||||
|
</exception>
|
||||||
|
<exception cref="T:System.InvalidOperationException">
|
||||||
|
Thrown if this instance of <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is already disposed, or if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.
|
||||||
|
May also be thrown if the browser process has crashed unexpectedly and left the control in an invalid state. We are considering throwing a different type of exception for this case in the future.
|
||||||
|
</exception>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment)">
|
||||||
|
<summary>
|
||||||
|
Explicitly trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||||
|
</summary>
|
||||||
|
<param name="environment">
|
||||||
|
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> that should be used to create the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||||
|
Creating your own environment gives you control over several options that affect how the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is initialized.
|
||||||
|
If you pass <c>null</c> then a default environment will be created and used automatically.
|
||||||
|
</param>
|
||||||
|
<returns>
|
||||||
|
A Task that represents the background initialization process.
|
||||||
|
When the task completes then the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property will be available for use (i.e. non-null).
|
||||||
|
Note that the control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked before the task completes
|
||||||
|
or on exceptions.
|
||||||
|
</returns>
|
||||||
|
<remarks>
|
||||||
|
Unless previous initialization has already failed, calling this method additional times with the same parameter will have no effect (any specified environment is ignored) and return the same Task as the first call.
|
||||||
|
Unless previous initialization has already failed, calling this method after initialization has been implicitly triggered by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property will have no effect if no environment is given
|
||||||
|
and simply return a Task representing that initialization already in progress.
|
||||||
|
Unless previous initialization has already failed, calling this method with a different environment after initialization has begun will result in an <see cref="T:System.ArgumentException"/>. For example, this can happen if you begin initialization
|
||||||
|
by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property and then call this method with a new environment, if you begin initialization with <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> and then call this method with a new
|
||||||
|
environment, or if you begin initialization with one environment and then call this method with no environment specified.
|
||||||
|
When this method is called after previous initialization has failed, it will trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> again.
|
||||||
|
Note that even though this method is asynchronous and returns a Task, it still must be called on the UI thread like most public functionality of most UI controls.
|
||||||
|
</remarks>
|
||||||
|
<exception cref="T:System.ArgumentException">
|
||||||
|
Thrown if this method is called with a different environment than when it was initialized. See Remarks for more info.
|
||||||
|
</exception>
|
||||||
|
<exception cref="T:System.InvalidOperationException">
|
||||||
|
Thrown if this instance of <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is already disposed, or if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.
|
||||||
|
May also be thrown if the browser process has crashed unexpectedly and left the control in an invalid state. We are considering throwing a different type of exception for this case in the future.
|
||||||
|
</exception>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.InitCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)">
|
||||||
|
<summary>
|
||||||
|
This is the private function which implements the actual background initialization task.
|
||||||
|
Cannot be called if the control is already initialized or has been disposed.
|
||||||
|
</summary>
|
||||||
|
<param name="environment">
|
||||||
|
The environment to use to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/>.
|
||||||
|
If that is null then a default environment is created with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> and its default parameters.
|
||||||
|
</param>
|
||||||
|
<param name="controllerOptions">
|
||||||
|
The controllerOptions to use to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/>.
|
||||||
|
If that is null then a default controllerOptions is created with its default parameters.
|
||||||
|
</param>
|
||||||
|
<returns>A task representing the background initialization process.</returns>
|
||||||
|
<remarks>All the event handlers added here need to be removed in <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.Dispose(System.Boolean)"/>.</remarks>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CreateParams">
|
||||||
|
<summary>
|
||||||
|
Protected CreateParams property. Used to set custom window styles to the forms HWND.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnVisibleChanged(System.EventArgs)">
|
||||||
|
<summary>
|
||||||
|
Protected VisibilityChanged handler.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnSizeChanged(System.EventArgs)">
|
||||||
|
<summary>
|
||||||
|
Protected SizeChanged handler.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Select(System.Boolean,System.Boolean)">
|
||||||
|
<summary>
|
||||||
|
Protected Select method: override this to capture tab direction when WebView control is activated
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.ProcessDialogKey(System.Windows.Forms.Keys)">
|
||||||
|
<summary>Processes a dialog key.</summary>
|
||||||
|
<param name="keyData">One of the <see cref="T:System.Windows.Forms.Keys" /> values that represents the key to process.</param>
|
||||||
|
<returns>
|
||||||
|
<see langword="true" /> if the key was processed by the control; otherwise, <see langword="false" />.</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnGotFocus(System.EventArgs)">
|
||||||
|
<summary>
|
||||||
|
Protected OnGotFocus handler.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnParentChanged(System.EventArgs)">
|
||||||
|
<summary>
|
||||||
|
Protected OnParentChanged handler.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.IsInitialized">
|
||||||
|
<summary>
|
||||||
|
True if initialization finished successfully and the control is not disposed yet.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GetSitedParentSite(System.Windows.Forms.Control)">
|
||||||
|
<summary>
|
||||||
|
Recursive retrieval of the parent control
|
||||||
|
</summary>
|
||||||
|
<param name="control">The control to get the parent for</param>
|
||||||
|
<returns>The root parent control</returns>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2">
|
||||||
|
<summary>
|
||||||
|
The underlying CoreWebView2. Use this property to perform more operations on the WebView2 content than is exposed
|
||||||
|
on the WebView2. This value is null until it is initialized and the object itself has undefined behaviour once the control is disposed.
|
||||||
|
You can force the underlying CoreWebView2 to
|
||||||
|
initialize via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> method.
|
||||||
|
</summary>
|
||||||
|
<exception cref="T:System.InvalidOperationException">Thrown if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.</exception>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactor">
|
||||||
|
<summary>
|
||||||
|
The zoom factor for the WebView.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.AllowExternalDrop">
|
||||||
|
<summary>
|
||||||
|
Enable/disable external drop.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.Source">
|
||||||
|
<summary>
|
||||||
|
The Source property is the URI of the top level document of the
|
||||||
|
WebView2. Setting the Source is equivalent to calling <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Navigate(System.String)"/>.
|
||||||
|
Setting the Source will trigger initialization of the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>, if not already initialized.
|
||||||
|
The default value of Source is <c>null</c>, indicating that the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.
|
||||||
|
</summary>
|
||||||
|
<exception cref="T:System.ArgumentException">Specified value is not an absolute <see cref="T:System.Uri"/>.</exception>
|
||||||
|
<exception cref="T:System.NotImplementedException">Specified value is <c>null</c> and the control is initialized.</exception>
|
||||||
|
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Navigate(System.String)"/>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CanGoForward">
|
||||||
|
<summary>
|
||||||
|
Returns true if the webview can navigate to a next page in the
|
||||||
|
navigation history via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.GoForward"/> method.
|
||||||
|
This is equivalent to the <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoForward"/>.
|
||||||
|
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this property is <c>false</c>.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoForward"/>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CanGoBack">
|
||||||
|
<summary>
|
||||||
|
Returns <c>true</c> if the webview can navigate to a previous page in the
|
||||||
|
navigation history via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.GoBack"/> method.
|
||||||
|
This is equivalent to the <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoBack"/>.
|
||||||
|
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this property is <c>false</c>.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoBack"/>
|
||||||
|
</member>
|
||||||
|
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.DefaultBackgroundColor">
|
||||||
|
<summary>
|
||||||
|
The default background color for the WebView.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.ExecuteScriptAsync(System.String)">
|
||||||
|
<summary>
|
||||||
|
Executes the provided script in the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||||
|
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.ExecuteScriptAsync(System.String)"/>.
|
||||||
|
</summary>
|
||||||
|
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
|
||||||
|
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
|
||||||
|
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.ExecuteScriptAsync(System.String)"/>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Reload">
|
||||||
|
<summary>
|
||||||
|
Reloads the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||||
|
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Reload"/>.
|
||||||
|
</summary>
|
||||||
|
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
|
||||||
|
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
|
||||||
|
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Reload"/>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GoForward">
|
||||||
|
<summary>
|
||||||
|
Navigates to the next page in navigation history.
|
||||||
|
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoForward"/>.
|
||||||
|
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoForward"/>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GoBack">
|
||||||
|
<summary>
|
||||||
|
Navigates to the previous page in navigation history.
|
||||||
|
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoBack"/>.
|
||||||
|
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoBack"/>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.NavigateToString(System.String)">
|
||||||
|
<summary>
|
||||||
|
Renders the provided HTML as the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||||
|
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.NavigateToString(System.String)"/>.
|
||||||
|
</summary>
|
||||||
|
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
|
||||||
|
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
|
||||||
|
<remarks>The <c>htmlContent</c> parameter may not be larger than 2 MB (2 * 1024 * 1024 bytes) in total size. The origin of the new page is <c>about:blank</c>.</remarks>
|
||||||
|
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.NavigateToString(System.String)"/>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Stop">
|
||||||
|
<summary>
|
||||||
|
Stops any in progress navigation in the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||||
|
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Stop"/>.
|
||||||
|
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Stop"/>
|
||||||
|
</member>
|
||||||
|
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted">
|
||||||
|
<summary>
|
||||||
|
This event is triggered either 1) when the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> has finished being initialized (regardless of how it was triggered or whether it succeeded) but before it is used for anything
|
||||||
|
OR 2) the initialization failed.
|
||||||
|
You should handle this event if you need to perform one time setup operations on the CoreWebView2 which you want to affect all of its usages
|
||||||
|
(e.g. adding event handlers, configuring settings, installing document creation scripts, adding host objects).
|
||||||
|
</summary>
|
||||||
|
<remarks>
|
||||||
|
This sender will be the WebView2 control, whose CoreWebView2 property will now be valid (i.e. non-null) for the first time
|
||||||
|
if <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs.IsSuccess"/> is true.
|
||||||
|
Unlikely this event can fire second time (after reporting initialization success first)
|
||||||
|
if the initialization is followed by navigation which fails.
|
||||||
|
</remarks>
|
||||||
|
</member>
|
||||||
|
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.NavigationStarting">
|
||||||
|
<summary>
|
||||||
|
NavigationStarting dispatches before a new navigate starts for the top
|
||||||
|
level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||||
|
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationStarting"/> event.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationStarting"/>
|
||||||
|
</member>
|
||||||
|
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.NavigationCompleted">
|
||||||
|
<summary>
|
||||||
|
NavigationCompleted dispatches after a navigate of the top level
|
||||||
|
document completes rendering either successfully or not.
|
||||||
|
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationCompleted"/> event.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationCompleted"/>
|
||||||
|
</member>
|
||||||
|
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.WebMessageReceived">
|
||||||
|
<summary>
|
||||||
|
WebMessageReceived dispatches after web content sends a message to the
|
||||||
|
app host via <c>chrome.webview.postMessage</c>.
|
||||||
|
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.WebMessageReceived"/> event.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.WebMessageReceived"/>
|
||||||
|
</member>
|
||||||
|
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.SourceChanged">
|
||||||
|
<summary>
|
||||||
|
SourceChanged dispatches after the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property changes. This may happen
|
||||||
|
during a navigation or if otherwise the script in the page changes the
|
||||||
|
URI of the document.
|
||||||
|
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.SourceChanged"/> event.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.SourceChanged"/>
|
||||||
|
</member>
|
||||||
|
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.ContentLoading">
|
||||||
|
<summary>
|
||||||
|
ContentLoading dispatches after a navigation begins to a new URI and the
|
||||||
|
content of that URI begins to render.
|
||||||
|
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.ContentLoading"/> event.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.ContentLoading"/>
|
||||||
|
</member>
|
||||||
|
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactorChanged">
|
||||||
|
<summary>
|
||||||
|
ZoomFactorChanged dispatches when the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactor"/> property changes.
|
||||||
|
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2Controller.ZoomFactorChanged"/> event.
|
||||||
|
</summary>
|
||||||
|
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2Controller.ZoomFactorChanged"/>
|
||||||
|
</member>
|
||||||
|
<member name="F:Microsoft.Web.WebView2.WinForms.WebView2.components">
|
||||||
|
<summary>
|
||||||
|
Required designer variable.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.InitializeComponent">
|
||||||
|
<summary>
|
||||||
|
Required method for Designer support - do not modify
|
||||||
|
the contents of this method with the code editor.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
</members>
|
||||||
|
</doc>
|
||||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
|||||||
|
-- 示例 1:无限找图点击
|
||||||
|
-- 用法:把要点击的按钮截图(F11 框选截图)保存到 images/start.png,再运行本脚本
|
||||||
|
-- 热键:F8 启动队列 / F9 暂停 / F10 停止;鼠标甩到左上角 (0,0) 紧急停止
|
||||||
|
|
||||||
|
log("开始找图循环")
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local x, y = findImage("images/start.png", 0.85)
|
||||||
|
|
||||||
|
if x >= 0 then
|
||||||
|
moveTo(x, y)
|
||||||
|
delay(80)
|
||||||
|
leftClick()
|
||||||
|
log("已点击 (" .. x .. "," .. y .. ")")
|
||||||
|
delayRandom(600, 1200)
|
||||||
|
else
|
||||||
|
log("未找到图片,1 秒后重试")
|
||||||
|
delay(1000)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- 示例 3:录制脚本的典型形态(由 F12 录制自动生成,可手工改写)
|
||||||
|
-- 说明:录制只会记录「按键 / 点击 / 滚动 / 拖动」,界面元素识别需自己补 findImage
|
||||||
|
|
||||||
|
-- 点击浏览器地址栏并输入网址
|
||||||
|
moveTo(742, 46)
|
||||||
|
leftClick(742, 46)
|
||||||
|
delay(120)
|
||||||
|
hotkey("Ctrl+A")
|
||||||
|
typeText("www.example.com")
|
||||||
|
keyPress("ENTER")
|
||||||
|
delay(2500)
|
||||||
|
|
||||||
|
-- 点击页面上的按钮(坐标来自录制,换机器或换分辨率需重录)
|
||||||
|
moveTo(512, 388)
|
||||||
|
leftClick(512, 388)
|
||||||
|
delay(800)
|
||||||
|
|
||||||
|
-- 滚动查看
|
||||||
|
wheel(-3)
|
||||||
|
delay(500)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- 示例 2:打开记事本并输入中文
|
||||||
|
-- 演示键盘输入(Unicode 注入,中文不受输入法影响)、组合键、循环
|
||||||
|
|
||||||
|
delay(500)
|
||||||
|
run("notepad.exe")
|
||||||
|
delay(1500)
|
||||||
|
activate("记事本")
|
||||||
|
|
||||||
|
typeText("按键精灵 · Lua 脚本自动输入")
|
||||||
|
keyPress("ENTER")
|
||||||
|
delay(200)
|
||||||
|
|
||||||
|
for i = 1, 5 do
|
||||||
|
typeText("第 " .. i .. " 行")
|
||||||
|
keyPress("ENTER")
|
||||||
|
delay(150)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 全选复制,读回剪贴板验证
|
||||||
|
hotkey("Ctrl+A")
|
||||||
|
delay(100)
|
||||||
|
hotkey("Ctrl+C")
|
||||||
|
delay(200)
|
||||||
|
local clip = clipGet()
|
||||||
|
log("剪贴板内容长度:" .. #clip)
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v10.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v10.0": {
|
||||||
|
"anjianjingling/1.0.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Web.WebView2": "1.0.3351.48",
|
||||||
|
"MoonSharp": "2.0.0",
|
||||||
|
"OpenCvSharp4": "4.10.0.20240616",
|
||||||
|
"OpenCvSharp4.runtime.win": "4.10.0.20240616",
|
||||||
|
"Microsoft.Web.WebView2.Core": "1.0.3351.48",
|
||||||
|
"Microsoft.Web.WebView2.WinForms": "1.0.3351.48",
|
||||||
|
"Microsoft.Web.WebView2.Wpf": "1.0.3351.48"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"anjianjingling.dll": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2/1.0.3351.48": {
|
||||||
|
"runtimeTargets": {
|
||||||
|
"runtimes/win-arm64/native/WebView2Loader.dll": {
|
||||||
|
"rid": "win-arm64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "1.0.3351.48"
|
||||||
|
},
|
||||||
|
"runtimes/win-x64/native/WebView2Loader.dll": {
|
||||||
|
"rid": "win-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "1.0.3351.48"
|
||||||
|
},
|
||||||
|
"runtimes/win-x86/native/WebView2Loader.dll": {
|
||||||
|
"rid": "win-x86",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "1.0.3351.48"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MoonSharp/2.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/netstandard1.6/MoonSharp.Interpreter.dll": {
|
||||||
|
"assemblyVersion": "2.0.0.0",
|
||||||
|
"fileVersion": "2.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OpenCvSharp4/4.10.0.20240616": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/OpenCvSharp.dll": {
|
||||||
|
"assemblyVersion": "1.0.0.0",
|
||||||
|
"fileVersion": "1.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"OpenCvSharp4.runtime.win/4.10.0.20240616": {
|
||||||
|
"runtimeTargets": {
|
||||||
|
"runtimes/win-x64/native/OpenCvSharpExtern.dll": {
|
||||||
|
"rid": "win-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/win-x64/native/opencv_videoio_ffmpeg4100_64.dll": {
|
||||||
|
"rid": "win-x64",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "2024.5.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/win-x86/native/OpenCvSharpExtern.dll": {
|
||||||
|
"rid": "win-x86",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"runtimes/win-x86/native/opencv_videoio_ffmpeg4100.dll": {
|
||||||
|
"rid": "win-x86",
|
||||||
|
"assetType": "native",
|
||||||
|
"fileVersion": "2024.5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2.Core/1.0.3351.48": {
|
||||||
|
"runtime": {
|
||||||
|
"Microsoft.Web.WebView2.Core.dll": {
|
||||||
|
"assemblyVersion": "1.0.3351.48",
|
||||||
|
"fileVersion": "1.0.3351.48"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2.WinForms/1.0.3351.48": {
|
||||||
|
"runtime": {
|
||||||
|
"Microsoft.Web.WebView2.WinForms.dll": {
|
||||||
|
"assemblyVersion": "1.0.3351.48",
|
||||||
|
"fileVersion": "1.0.3351.48"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2.Wpf/1.0.3351.48": {
|
||||||
|
"runtime": {
|
||||||
|
"Microsoft.Web.WebView2.Wpf.dll": {
|
||||||
|
"assemblyVersion": "1.0.3351.48",
|
||||||
|
"fileVersion": "1.0.3351.48"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"anjianjingling/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2/1.0.3351.48": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-UIzXdl3x3H2MX2es7+BHYT3pLkLoT7090omqla9S3MQQCma0slwiY+CXBM6x0LmdLTpksoQ6XUqOAXrtxjtklQ==",
|
||||||
|
"path": "microsoft.web.webview2/1.0.3351.48",
|
||||||
|
"hashPath": "microsoft.web.webview2.1.0.3351.48.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"MoonSharp/2.0.0": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-uiAcRh7d+53k3xW9pFDJfAFVw4RnjHVCJG05M3oPAVEVwPtFavhg1H/IpC6So4X1j9kJlzuLlA3OghhPcIvc5A==",
|
||||||
|
"path": "moonsharp/2.0.0",
|
||||||
|
"hashPath": "moonsharp.2.0.0.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"OpenCvSharp4/4.10.0.20240616": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-+iaFSUq0Cp+B9s1sUqDwB3PvQ5HdWWvRE50WsAw1PL8smD3VZNbe0P786E4Yfu7LHM7jeqkulOwjk2CoihsZeg==",
|
||||||
|
"path": "opencvsharp4/4.10.0.20240616",
|
||||||
|
"hashPath": "opencvsharp4.4.10.0.20240616.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"OpenCvSharp4.runtime.win/4.10.0.20240616": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-dpQNIR6PpeB/oZM0e0fZpt9lOS3Htjm6ZjtOgLyxN7QwjRekbNevrmO8VbgezUoetk68CRwxYZGaymCm2vt8Sg==",
|
||||||
|
"path": "opencvsharp4.runtime.win/4.10.0.20240616",
|
||||||
|
"hashPath": "opencvsharp4.runtime.win.4.10.0.20240616.nupkg.sha512"
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2.Core/1.0.3351.48": {
|
||||||
|
"type": "reference",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2.WinForms/1.0.3351.48": {
|
||||||
|
"type": "reference",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
},
|
||||||
|
"Microsoft.Web.WebView2.Wpf/1.0.3351.48": {
|
||||||
|
"type": "reference",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net10.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "10.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.WindowsDesktop.App",
|
||||||
|
"version": "10.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false,
|
||||||
|
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"description" : "AutoLaunch Protocols Preregistration",
|
||||||
|
"name" : "Protocol Preregistration",
|
||||||
|
"version" : "1.0.0.12"
|
||||||
|
}
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"https://.get.microsoft.com",
|
||||||
|
"https://.apps.microsoft.com"
|
||||||
|
],
|
||||||
|
"protocol": "ms-windows-store"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"https://.onedrive.com",
|
||||||
|
"https://.onedrive.live.com",
|
||||||
|
"https://sharepoint.com"
|
||||||
|
],
|
||||||
|
"protocol": "ms-word"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"^https://[a-z0-9-]*word-edit\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*word-view\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*onenote\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*eap\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*shared\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*afhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*vhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*optin\\.online\\.office\\.com/$"
|
||||||
|
],
|
||||||
|
"use_regex": true,
|
||||||
|
"protocol": "ms-word"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"https://.onedrive.com",
|
||||||
|
"https://.onedrive.live.com",
|
||||||
|
"https://sharepoint.com"
|
||||||
|
],
|
||||||
|
"protocol": "ms-excel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"^https://[a-z0-9-]*excel\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*onenote\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*eap\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*shared\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*afhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*vhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*optin\\.online\\.office\\.com/$"
|
||||||
|
],
|
||||||
|
"use_regex": true,
|
||||||
|
"protocol": "ms-excel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"https://.onedrive.com",
|
||||||
|
"https://.onedrive.live.com",
|
||||||
|
"https://sharepoint.com"
|
||||||
|
],
|
||||||
|
"protocol": "ms-powerpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"^https://[a-z0-9-]*powerpoint\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*onenote\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*eap\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*shared\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*afhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*vhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*optin\\.online\\.office\\.com/$"
|
||||||
|
],
|
||||||
|
"use_regex": true,
|
||||||
|
"protocol": "ms-powerpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"https://.onedrive.com",
|
||||||
|
"https://.onedrive.live.com",
|
||||||
|
"https://sharepoint.com"
|
||||||
|
],
|
||||||
|
"protocol": "ms-visio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"^https://[a-z0-9-]*visio\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*onenote\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*eap\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*shared\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*afhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*vhs\\.officeapps\\.live\\.com/$",
|
||||||
|
"^https://[a-z0-9-]*optin\\.online\\.office\\.com/$"
|
||||||
|
],
|
||||||
|
"use_regex": true,
|
||||||
|
"protocol": "ms-visio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"https://www.microsoft.com",
|
||||||
|
"https://copilot.microsoft.com"
|
||||||
|
],
|
||||||
|
"protocol": "ms-copilot"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warn": [
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"protocol": "ms-test-warn"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"block": [
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"protocol": "ms-test-block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origins": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"protocol": "applescript"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"description": "Microsoft CRLSet",
|
||||||
|
"name": "MicrosoftCRLSet",
|
||||||
|
"version": "6498.2026.10.20"
|
||||||
|
}
|
||||||
Whitespace-only changes.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
level=none expiry=0
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+2
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
Binary file not shown.
Whitespace-only changes.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"epochs": [ {
|
||||||
|
"calculation_time": "13437158859401862",
|
||||||
|
"config_version": 0,
|
||||||
|
"model_version": "0",
|
||||||
|
"padded_top_topics_start_index": 0,
|
||||||
|
"taxonomy_version": 0,
|
||||||
|
"top_topics_and_observing_domains": [ ]
|
||||||
|
} ],
|
||||||
|
"hex_encoded_hmac_key": "DCC451D787D10A2973A63C81E3FC29CE61D5A03C7E2C264DBC6330B6CAA8E40C",
|
||||||
|
"next_scheduled_calculation_time": "13437763659401943"
|
||||||
|
}
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
@@ -0,0 +1 @@
|
|||||||
|
婁<M
|
||||||
BIN
Binary file not shown.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 404 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user