using System.ComponentModel; using System.Diagnostics; using anjianjingling.Browser; using anjianjingling.Capture; using anjianjingling.Common; using anjianjingling.Input; using anjianjingling.Lua; using anjianjingling.Native; using anjianjingling.Vision; using anjianjingling.Window; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; namespace anjianjingling.Forms { /// /// 主窗体。布局:左侧任务列表 / 中间任务详情 / 右侧插件 / 下方日志。 /// 全 C# 实现:Lua(MoonSharp)+ 自绘积木 + 键鼠录制 + 屏幕录像。 /// public sealed class MainForm : Form, ILuaHost { // ===================== 模块 ===================== private readonly MouseInput _mouse = new(); private readonly KeyboardInput _keyboard = new(); private readonly WindowManager _windows = new(); private readonly ScreenCapture _capture = new(); private readonly ImageFinder _finder = new(); private readonly ColorFinder _colorFinder = new(); private readonly LogBus _log = new(); private readonly LuaEngine _engine; private readonly InputRecorder _recorder = new(); private readonly ScreenRecorder _screenRec = new(); // ===================== 左侧:任务列表 ===================== private readonly BindingList _tasks = new(); private readonly ListBox _taskList = new(); private LuaTask? _current; private bool _sync; /// true = 正在脚本⇄积木互相灌入,抑制联动避免死循环。 private bool _converting; // ===================== 中间:任务详情 ===================== private readonly TabControl _detailTabs = new(); private LuaEditor _editor = null!; private BlockCanvas _canvas = null!; private TreeView _lib = null!; private Panel _paramPanel = null!; private readonly Label _lblBlockInfo = new(); private readonly TextBox _txtName = new(); private readonly CheckBox _chkEnabled = new(); private readonly CheckBox _chkInfinite = new(); private readonly NumericUpDown _numLoop = new(); private readonly NumericUpDown _numInterval = new(); private readonly Label _lblSource = new(); private readonly ComboBox _cboHotkey = new(); private const int HK_TASK_BASE = 200; private readonly Dictionary _taskHotkeys = new(); // ===================== 右侧:插件 ===================== private readonly FlowLayoutPanel _pluginPanel = new(); // ===================== 下方:日志 ===================== private readonly RichTextBox _logBox = new(); // ===================== 工具栏 / 状态栏 ===================== private Button _btnStart = null!, _btnPause = null!, _btnStop = null!; private readonly CheckBox _chkJitter = new(); private volatile bool _jitter = true; private readonly ToolStripStatusLabel _ssState = new(); private readonly ToolStripStatusLabel _ssCaret = new(); private readonly ToolStripStatusLabel _ssHint = new(); // ===================== 录制 / 录屏 ===================== private Label _lblRecState = null!; private Button _btnRecToggle = null!; private TextBox _txtRecPreview = null!; private readonly CheckBox _chkRecMove = new(); private readonly CheckBox _chkRecText = new(); private readonly NumericUpDown _numRecDelay = new(); private Label _lblCapState = null!; private readonly NumericUpDown _numFps = new(); private readonly CheckBox _chkCursor = new(); private Rectangle? _capRegion; // ===================== 浏览器 ===================== private WebView2 _web = new(); private WebViewBridge? _bridge; private Task? _webInit; private readonly TextBox _txtUrl = new(); private readonly System.Windows.Forms.Timer _logTimer = new(); private readonly System.Windows.Forms.Timer _recTimer = new(); // ===================== 热键 ===================== private const int HK_START = 100, HK_PAUSE = 101, HK_STOP = 102, HK_SHOT = 103, HK_REC = 104; // ===================== 文件 ===================== private string? _luaPath; private string? _blockPath; private string? _taskPath; // ===================== 配色 ===================== private static readonly Color C_Bg = Color.FromArgb(245, 247, 250); private static readonly Color C_Panel = Color.White; private static readonly Color C_Head = Color.FromArgb(233, 238, 247); private static readonly Color C_HeadText = Color.FromArgb(45, 60, 85); private static readonly Color C_Sub = Color.FromArgb(130, 140, 158); private static readonly Color C_Blue = Color.FromArgb(37, 99, 235); private static readonly Color C_Green = Color.FromArgb(47, 143, 78); private static readonly Color C_Red = Color.FromArgb(200, 70, 60); private readonly Font _fName = new("Microsoft YaHei UI", 10F, FontStyle.Bold); private readonly Font _fSub = new("Microsoft YaHei UI", 8F); private readonly Font _fHead = new("Microsoft YaHei UI", 9.5F, FontStyle.Bold); public MainForm() { _engine = new LuaEngine(this); Text = "按键精灵 · 任务 / 脚本 / 积木 / 录制"; ClientSize = new Size(1360, 860); MinimumSize = new Size(1080, 720); StartPosition = FormStartPosition.CenterScreen; Font = new Font("Microsoft YaHei UI", 9F); BackColor = C_Bg; BuildMenu(); BuildLayout(); _logTimer.Interval = 150; _logTimer.Tick += (s, e) => FlushLog(); _logTimer.Start(); _recTimer.Interval = 500; _recTimer.Tick += (s, e) => UpdateCapStatus(); _recTimer.Start(); _engine.StateChanged += OnEngineState; _recorder.StateChanged += OnRecordState; _tasks.Add(new LuaTask { Name = "演示任务", Infinite = false, LoopCount = 1, IntervalMs = 1000, Script = DefaultScript, Source = "内置示例" }); if (_tasks.Count > 0) _taskList.SelectedIndex = 0; Load += (s, e) => { RegisterHotkeys(); RebindTaskHotkeys(); }; Shown += async (s, e) => { try { await EnsureWeb(); _log.Write("内嵌浏览器已就绪"); } catch (Exception ex) { _log.Write("WebView2 初始化失败(不影响其他功能):" + ex.Message); } }; FormClosing += (s, e) => { try { _engine.Stop(); } catch { } try { _recorder.Abort(); } catch { } try { _screenRec.Stop(); } catch { } UnregisterHotkeys(); }; } // ===================== 菜单 ===================== private void BuildMenu() { var menu = new MenuStrip { Font = new Font("Microsoft YaHei UI", 9F) }; var mFile = new ToolStripMenuItem("文件(&F)"); mFile.DropDownItems.Add("新建任务", null, (s, e) => AddTask()); mFile.DropDownItems.Add("打开脚本…", null, (s, e) => OpenLua()); mFile.DropDownItems.Add("保存脚本", null, (s, e) => SaveLua(false)); mFile.DropDownItems.Add("另存为…", null, (s, e) => SaveLua(true)); mFile.DropDownItems.Add(new ToolStripSeparator()); mFile.DropDownItems.Add("打开任务列表…", null, (s, e) => OpenTaskList()); mFile.DropDownItems.Add("保存任务列表", null, (s, e) => SaveTaskList()); mFile.DropDownItems.Add(new ToolStripSeparator()); mFile.DropDownItems.Add("退出", null, (s, e) => Close()); var mBlock = new ToolStripMenuItem("积木(&B)"); mBlock.DropDownItems.Add("新建积木", null, (s, e) => _canvas.ClearAll()); mBlock.DropDownItems.Add("打开积木…", null, (s, e) => OpenBlocks()); mBlock.DropDownItems.Add("保存积木", null, (s, e) => SaveBlocks(false)); mBlock.DropDownItems.Add(new ToolStripSeparator()); mBlock.DropDownItems.Add("积木 → 脚本", null, (s, e) => BlocksToLua()); mBlock.DropDownItems.Add("脚本 → 积木", null, (s, e) => { _detailTabs.SelectedIndex = 1; SyncScriptToBlocks(); }); mBlock.DropDownItems.Add("运行积木", null, (s, e) => RunBlocks()); var mTool = new ToolStripMenuItem("工具(&O)"); mTool.DropDownItems.Add("框选截图 (F11)", null, (s, e) => TakeClipShot()); mTool.DropDownItems.Add("坐标取色器", null, (s, e) => OpenPicker()); mTool.DropDownItems.Add("找图测试…", null, (s, e) => TestFindImage()); mTool.DropDownItems.Add(new ToolStripSeparator()); mTool.DropDownItems.Add("开始/停止操作录制 (F12)", null, (s, e) => ToggleRecord()); mTool.DropDownItems.Add("全屏录像", null, (s, e) => StartCapture(null)); var mHelp = new ToolStripMenuItem("帮助(&H)"); mHelp.DropDownItems.Add("Lua 语法速查", null, (s, e) => ShowHelp()); mHelp.DropDownItems.Add("关于", null, (s, e) => MessageBox.Show(this, "按键精灵 · 全 C# 实现\n\n" + "脚本引擎:Lua(MoonSharp 纯托管)\n" + "积木编辑:WinForms 自绘拖拽控件\n" + "录制:低级键鼠钩子 → 生成 Lua\n" + "录屏:GDI 抓帧 + AVI(MJPG) 封装\n\n" + "热键:F8 启动 / F9 暂停 / F10 停止\n" + "   F11 框选截图 / F12 操作录制\n" + "鼠标甩到屏幕左上角 (0,0) 紧急停止", "关于", MessageBoxButtons.OK, MessageBoxIcon.Information)); menu.Items.AddRange(new ToolStripItem[] { mFile, mBlock, mTool, mHelp }); MainMenuStrip = menu; Controls.Add(menu); } // ===================== 总布局 ===================== private void BuildLayout() { // --- 主分割:上=工作区,下=日志 --- var mainSplit = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Horizontal, FixedPanel = FixedPanel.Panel2, SplitterWidth = 5, BackColor = C_Bg }; bool mainInit = false; mainSplit.Layout += (s, e) => { if (mainInit || mainSplit.Height < 260) return; try { mainSplit.SplitterDistance = mainSplit.Height - 190; mainInit = true; } catch { } }; // --- 工作区:左 任务列表 | 右(详情 + 插件) --- var bodySplit = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Vertical, FixedPanel = FixedPanel.Panel1, SplitterWidth = 5, BackColor = C_Bg }; bool bodyInit = false; bodySplit.Layout += (s, e) => { if (bodyInit || bodySplit.Width < 400) return; try { bodySplit.SplitterDistance = 248; bodyInit = true; } catch { } }; // --- 右侧:详情 | 插件 --- var rightSplit = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Vertical, FixedPanel = FixedPanel.Panel2, SplitterWidth = 5, BackColor = C_Bg }; bool rightInit = false; rightSplit.Layout += (s, e) => { if (rightInit || rightSplit.Width < 500) return; try { rightSplit.SplitterDistance = rightSplit.Width - 234; rightInit = true; } catch { } }; bodySplit.Panel1.Controls.Add(BuildTaskListPane()); rightSplit.Panel1.Controls.Add(BuildDetailPane()); rightSplit.Panel2.Controls.Add(BuildPluginPane()); bodySplit.Panel2.Controls.Add(rightSplit); mainSplit.Panel1.Controls.Add(bodySplit); mainSplit.Panel2.Controls.Add(BuildLogPane()); // Dock 顺序:Fill → Bottom → Top(工具栏) → Top(菜单) Controls.Add(mainSplit); Controls.Add(BuildStatusBar()); Controls.Add(BuildToolbar()); // Controls 索引越大越靠外:把菜单顶到最上面 if (Controls.OfType().FirstOrDefault() is { } ms) Controls.SetChildIndex(ms, Controls.Count - 1); } // ===================== 左:任务列表 ===================== private Control BuildTaskListPane() { var wrap = new Panel { Dock = DockStyle.Fill, BackColor = C_Panel }; var head = new Panel { Dock = DockStyle.Top, Height = 34, BackColor = C_Head }; head.Controls.Add(new Label { Text = "任务列表", Dock = DockStyle.Left, Width = 120, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(10, 0, 0, 0), Font = _fHead, ForeColor = C_HeadText }); var btnAdd = new Button { Text = "+", Dock = DockStyle.Right, Width = 34, FlatStyle = FlatStyle.Flat, Font = new Font("Microsoft YaHei UI", 11F), Cursor = Cursors.Hand, ForeColor = C_HeadText, BackColor = C_Head }; btnAdd.FlatAppearance.BorderSize = 0; btnAdd.Click += (s, e) => AddTask(); head.Controls.Add(btnAdd); _taskList.Dock = DockStyle.Fill; _taskList.BorderStyle = BorderStyle.None; _taskList.DrawMode = DrawMode.OwnerDrawFixed; _taskList.ItemHeight = 48; _taskList.BackColor = C_Panel; _taskList.Font = new Font("Microsoft YaHei UI", 9F); _taskList.IntegralHeight = false; _taskList.HorizontalScrollbar = false; _taskList.DrawItem += TaskListDraw; _taskList.SelectedIndexChanged += (s, e) => { if (_sync) return; SelectTask(_taskList.SelectedItem as LuaTask); }; _taskList.MouseDown += (s, e) => { int idx = _taskList.IndexFromPoint(e.Location); if (idx < 0) return; var t = idx < _tasks.Count ? _tasks[idx] : null; // 点右侧勾选框 = 切换启用;其余 = 选中 if (t != null && e.X >= _taskList.ClientSize.Width - 32 && e.Y % 48 >= 16) { t.Enabled = !t.Enabled; if (t == _current) _chkEnabled.Checked = t.Enabled; _taskList.Invalidate(); return; } _taskList.SelectedIndex = idx; }; var ctx = new ContextMenuStrip { Font = new Font("Microsoft YaHei UI", 9F) }; ctx.Items.Add("新建任务", null, (s, e) => AddTask()); ctx.Items.Add("复制任务", null, (s, e) => DuplicateTask()); ctx.Items.Add("删除任务", null, (s, e) => DeleteTask()); ctx.Items.Add(new ToolStripSeparator()); ctx.Items.Add("上移", null, (s, e) => MoveTask(-1)); ctx.Items.Add("下移", null, (s, e) => MoveTask(1)); ctx.Items.Add(new ToolStripSeparator()); ctx.Items.Add("立即运行此任务", null, (s, e) => RunSelectedTask()); _taskList.ContextMenuStrip = ctx; var foot = new Panel { Dock = DockStyle.Bottom, Height = 26, BackColor = C_Head }; var lblCount = new Label { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(10, 0, 0, 0), Font = _fSub, ForeColor = C_Sub }; foot.Controls.Add(lblCount); void RefreshCount() => lblCount.Text = $"共 {_tasks.Count} 个任务 · 已勾选 {_tasks.Count(t => t.Enabled)} 个"; RefreshCount(); _tasks.ListChanged += (s, e) => { RefreshTaskList(); RefreshCount(); RebindTaskHotkeys(); }; wrap.Controls.Add(_taskList); wrap.Controls.Add(foot); wrap.Controls.Add(head); return wrap; } private void TaskListDraw(object? sender, DrawItemEventArgs e) { if (e.Index < 0 || e.Index >= _tasks.Count) return; var t = _tasks[e.Index]; var g = e.Graphics; bool sel = (e.State & DrawItemState.Selected) != 0; using (var bg = new SolidBrush(sel ? Color.FromArgb(226, 236, 252) : (e.Index % 2 == 0 ? C_Panel : Color.FromArgb(250, 251, 253)))) g.FillRectangle(bg, e.Bounds); Color bar = ReferenceEquals(t, _current) ? C_Blue : (t.Enabled ? Color.FromArgb(120, 190, 150) : Color.FromArgb(205, 210, 220)); using (var bb = new SolidBrush(bar)) g.FillRectangle(bb, new Rectangle(e.Bounds.X, e.Bounds.Y + 9, 3, e.Bounds.Height - 18)); using (var fc = new SolidBrush(sel ? Color.FromArgb(20, 40, 80) : Color.FromArgb(40, 48, 60))) g.DrawString(t.Name, _fName, fc, new RectangleF(e.Bounds.X + 12, e.Bounds.Y + 6, e.Bounds.Width - 22, 20)); string sub = (t.Enabled ? "" : "已停用 · ") + (t.Infinite ? "无限循环" : $"循环 {t.LoopCount} 次") + $" · 间隔 {t.IntervalMs}ms"; using (var fc2 = new SolidBrush(C_Sub)) g.DrawString(sub, _fSub, fc2, new RectangleF(e.Bounds.X + 12, e.Bounds.Y + 27, e.Bounds.Width - 22, 18)); // 右侧启用勾选框(点击切换) int bx = e.Bounds.Right - 30; int by = e.Bounds.Top + (e.Bounds.Height - 16) / 2; var cb = new Rectangle(bx, by, 16, 16); using (var cbBg = new SolidBrush(t.Enabled ? C_Green : Color.White)) g.FillRectangle(cbBg, cb); using (var cbPen = new Pen(t.Enabled ? Color.FromArgb(38, 112, 62) : Color.FromArgb(180, 188, 200))) g.DrawRectangle(cbPen, cb); if (t.Enabled) { using var checkPen = new Pen(Color.White, 1.8f); g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.DrawLine(checkPen, cb.Left + 4, cb.Top + 8, cb.Left + 7, cb.Top + 12); g.DrawLine(checkPen, cb.Left + 7, cb.Top + 12, cb.Left + 12, cb.Top + 4); } using (var pen = new Pen(Color.FromArgb(238, 241, 246))) g.DrawLine(pen, e.Bounds.Left + 10, e.Bounds.Bottom - 1, e.Bounds.Right - 6, e.Bounds.Bottom - 1); } // ===================== 中:任务详情 ===================== private Control BuildDetailPane() { var wrap = new Panel { Dock = DockStyle.Fill, BackColor = C_Bg }; // ---- 任务属性条 ---- var head = new Panel { Dock = DockStyle.Top, Height = 96, BackColor = C_Panel, Padding = new Padding(10, 7, 10, 4) }; var row1 = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 30, WrapContents = false }; row1.Controls.Add(new Label { Text = "任务名", AutoSize = true, Margin = new Padding(2, 7, 8, 0), ForeColor = C_HeadText }); _txtName.Width = 210; _txtName.Height = 26; _txtName.Margin = new Padding(0, 3, 12, 0); _txtName.Font = new Font("Microsoft YaHei UI", 9.5F); _txtName.TextChanged += (s, e) => { if (_sync || _current == null) return; _current.Name = _txtName.Text; _taskList.Invalidate(); }; row1.Controls.Add(_txtName); _chkEnabled.Text = "启用"; _chkEnabled.AutoSize = true; _chkEnabled.Margin = new Padding(0, 7, 12, 0); _chkEnabled.CheckedChanged += (s, e) => { if (_sync || _current == null) return; _current.Enabled = _chkEnabled.Checked; _taskList.Invalidate(); }; row1.Controls.Add(_chkEnabled); _chkInfinite.Text = "无限循环"; _chkInfinite.AutoSize = true; _chkInfinite.Margin = new Padding(0, 7, 8, 0); _chkInfinite.CheckedChanged += (s, e) => { if (_sync || _current == null) return; _current.Infinite = _chkInfinite.Checked; _numLoop.Enabled = !_chkInfinite.Checked; if (!_chkInfinite.Checked) _current.LoopCount = (int)_numLoop.Value; _taskList.Invalidate(); }; row1.Controls.Add(_chkInfinite); _numLoop.Minimum = 1; _numLoop.Maximum = 999999; _numLoop.Width = 70; _numLoop.Margin = new Padding(0, 4, 4, 0); _numLoop.ValueChanged += (s, e) => { if (_sync || _current == null) return; _current.LoopCount = (int)_numLoop.Value; _taskList.Invalidate(); }; row1.Controls.Add(_numLoop); row1.Controls.Add(new Label { Text = "次 间隔", AutoSize = true, Margin = new Padding(0, 7, 6, 0), ForeColor = C_HeadText }); _numInterval.Minimum = 0; _numInterval.Maximum = 3600000; _numInterval.Increment = 100; _numInterval.Width = 80; _numInterval.Margin = new Padding(0, 4, 4, 0); _numInterval.ValueChanged += (s, e) => { if (_sync || _current == null) return; _current.IntervalMs = (int)_numInterval.Value; _taskList.Invalidate(); }; row1.Controls.Add(_numInterval); row1.Controls.Add(new Label { Text = "ms", AutoSize = true, Margin = new Padding(0, 7, 14, 0), ForeColor = C_HeadText }); row1.Controls.Add(new Label { Text = "启动热键", AutoSize = true, Margin = new Padding(0, 7, 6, 0), ForeColor = C_HeadText }); _cboHotkey.DropDownStyle = ComboBoxStyle.DropDownList; _cboHotkey.Width = 74; _cboHotkey.Margin = new Padding(0, 4, 0, 0); _cboHotkey.Items.AddRange(new object[] { "无", "F1", "F2", "F3", "F4", "F5", "F6", "F7" }); _cboHotkey.SelectedIndexChanged += (s, e) => { if (_sync || _current == null) return; _current.Hotkey = _cboHotkey.SelectedIndex <= 0 ? "" : (string)_cboHotkey.SelectedItem!; _lblSource.Text = $"来源 {((string.IsNullOrEmpty(_current.Source) ? "内置" : _current.Source))} · 热键 {(string.IsNullOrEmpty(_current.Hotkey) ? "未绑定" : _current.Hotkey)}"; RebindTaskHotkeys(); }; row1.Controls.Add(_cboHotkey); var row2 = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 42, WrapContents = false, Padding = new Padding(2, 2, 0, 0) }; _lblSource.Text = "未选中任务"; ForeColor = C_Sub; Font = _fSub; _lblSource.AutoSize = false; _lblSource.Size = new Size(240, 20); _lblSource.TextAlign = ContentAlignment.MiddleLeft; _lblSource.AutoEllipsis = true; _lblSource.Margin = new Padding(0, 5, 10, 0); row2.Controls.Add(_lblSource); row2.Controls.Add(MakeButton("▶ 运行此任务", (s, e) => RunSelectedTask(), C_Green)); row2.Controls.Add(MakeButton("校验语法", (s, e) => ValidateScript())); row2.Controls.Add(MakeButton("另存脚本", (s, e) => SaveLua(true))); // 切到积木页时自动把脚本解析成积木(脚本 ⇄ 积木 单一数据源) _detailTabs.SelectedIndexChanged += (s, e) => { if (_detailTabs.SelectedIndex == 1) SyncScriptToBlocks(); }; head.Controls.Add(row1); head.Controls.Add(row2); // ---- 详情页签 ---- _detailTabs.Dock = DockStyle.Fill; _detailTabs.Font = new Font("Microsoft YaHei UI", 9.5F); _detailTabs.TabPages.Add(BuildScriptPage()); _detailTabs.TabPages.Add(BuildBlockPage()); _detailTabs.TabPages.Add(BuildRecordPage()); _detailTabs.TabPages.Add(BuildBrowserPage()); wrap.Controls.Add(_detailTabs); wrap.Controls.Add(head); return wrap; } private TabPage BuildScriptPage() { var page = new TabPage("脚本代码"); var bar = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 38, Padding = new Padding(6, 5, 0, 0), WrapContents = false, BackColor = C_Bg }; bar.Controls.Add(MakeButton("▶ 运行 (F8)", (s, e) => RunEditorScript(), C_Green)); bar.Controls.Add(MakeButton("校验", (s, e) => ValidateScript())); bar.Controls.Add(MakeButton("打开", (s, e) => OpenLua())); bar.Controls.Add(MakeButton("保存", (s, e) => SaveLua(false))); bar.Controls.Add(MakeButton("转成积木 ▶", (s, e) => _detailTabs.SelectedIndex = 1, C_Blue)); bar.Controls.Add(MakeButton("插入示例", (s, e) => _editor.Script = DefaultScript)); bar.Controls.Add(MakeButton("A+", (s, e) => _editor.Inner.Font = new Font("Consolas", _editor.Inner.Font.Size + 1))); bar.Controls.Add(MakeButton("A-", (s, e) => _editor.Inner.Font = new Font("Consolas", Math.Max(8, _editor.Inner.Font.Size - 1)))); _editor = new LuaEditor { Dock = DockStyle.Fill }; _editor.CaretMoved += (line, col) => _ssCaret.Text = $"行 {line}, 列 {col}"; _editor.ScriptChanged += (s, e) => { if (_current != null) _current.Script = _editor.Script; }; page.Controls.Add(_editor); page.Controls.Add(bar); return page; } private TabPage BuildBlockPage() { var page = new TabPage("积木编程"); var bar = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 74, Padding = new Padding(6, 5, 0, 0), WrapContents = true, BackColor = C_Bg }; bar.Controls.Add(MakeButton("◀ 从脚本载入", (s, e) => SyncScriptToBlocks(), C_Blue)); bar.Controls.Add(MakeButton("▶ 运行积木", (s, e) => RunBlocks(), C_Green)); bar.Controls.Add(MakeButton("上移", (s, e) => _canvas.MoveSelected(-1))); bar.Controls.Add(MakeButton("下移", (s, e) => _canvas.MoveSelected(1))); bar.Controls.Add(MakeButton("复制", (s, e) => _canvas.DuplicateSelected())); bar.Controls.Add(MakeButton("删除", (s, e) => _canvas.RemoveSelected())); bar.Controls.Add(MakeButton("保存积木", (s, e) => SaveBlocks(false))); bar.Controls.Add(MakeButton("清空", (s, e) => _canvas.ClearAll())); _lib = new TreeView { Dock = DockStyle.Fill, Font = new Font("Microsoft YaHei UI", 9F), FullRowSelect = true, HideSelection = false, ShowLines = false, ItemHeight = 24, BorderStyle = BorderStyle.None, BackColor = Color.FromArgb(252, 253, 255) }; BuildLibrary(); _lib.ItemDrag += (s, e) => { if ((e.Item as TreeNode)?.Tag is string key) DoDragDrop(new DataObject(BlockCanvas.DragFormat, key), DragDropEffects.Copy); }; _lib.DoubleClick += (s, e) => { if (_lib.SelectedNode?.Tag is string key) AddBlockFromLibrary(key); }; _lib.AfterSelect += (s, e) => { if (_lib.SelectedNode?.Tag is string key) { var def = BlockCatalog.Find(key); if (def != null && def.Help.Length > 0) _lblBlockInfo.Text = def.Help; } }; _canvas = new BlockCanvas { Dock = DockStyle.Fill }; _canvas.SelectionChanged += b => BuildParamPanel(b); _canvas.EditRequested += b => BuildParamPanel(b); _canvas.Changed += () => { _lblBlockInfo.Text = $"共 {CountBlocks(_canvas.Blocks)} 个积木"; if (_converting || _current == null) return; // 积木改动 → 立刻回写脚本(脚本才是任务的唯一代码) _converting = true; try { _editor.Script = BlockCatalog.ToLua(_canvas.Blocks); } finally { _converting = false; } }; _paramPanel = new Panel { Dock = DockStyle.Right, Width = 230, BackColor = Color.FromArgb(252, 253, 255), Padding = new Padding(10), AutoScroll = true }; var leftLib = new Panel { Dock = DockStyle.Left, Width = 198 }; leftLib.Controls.Add(_lib); leftLib.Controls.Add(PaneHeader("命令库(双击 / 拖入)", DockStyle.Top)); var center = new Panel { Dock = DockStyle.Fill }; center.Controls.Add(_canvas); _lblBlockInfo.Text = "从左侧命令库双击或拖入积木"; _lblBlockInfo.Dock = DockStyle.Bottom; _lblBlockInfo.Height = 24; _lblBlockInfo.TextAlign = ContentAlignment.MiddleLeft; _lblBlockInfo.Padding = new Padding(10, 0, 0, 0); _lblBlockInfo.BackColor = C_Head; _lblBlockInfo.ForeColor = Color.FromArgb(90, 105, 130); _lblBlockInfo.Font = _fSub; var rightWrap = new Panel { Dock = DockStyle.Right, Width = 230 }; rightWrap.Controls.Add(_paramPanel); rightWrap.Controls.Add(PaneHeader("参数", DockStyle.Top)); page.Controls.Add(center); page.Controls.Add(rightWrap); page.Controls.Add(leftLib); page.Controls.Add(_lblBlockInfo); page.Controls.Add(bar); BuildParamPanel(null); return page; } private TabPage BuildRecordPage() { var page = new TabPage("录制 / 录屏"); var split = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Horizontal, SplitterWidth = 5, BackColor = C_Bg }; bool init = false; split.Layout += (s, e) => { if (init || split.Height < 200) return; try { split.SplitterDistance = (int)(split.Height * 0.64); init = true; } catch { } }; // ---- 操作录制 ---- var gbRec = new GroupBox { Text = " 操作录制(键盘鼠标 → 生成 Lua)", Dock = DockStyle.Fill, Padding = new Padding(8), Font = new Font("Microsoft YaHei UI", 9F) }; var recBar = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 40, WrapContents = false }; _btnRecToggle = MakeButton("● 开始录制 (F12)", (s, e) => ToggleRecord(), C_Red); _lblRecState = new Label { Text = "未录制", AutoSize = true, Margin = new Padding(14, 9, 0, 0), ForeColor = C_Sub }; recBar.Controls.Add(_btnRecToggle); recBar.Controls.Add(_lblRecState); var optBar = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 34, WrapContents = false }; _chkRecMove.Text = "录鼠标移动"; _chkRecMove.Checked = true; _chkRecMove.AutoSize = true; _chkRecMove.Margin = new Padding(4, 7, 0, 0); _chkRecText.Text = "字符聚合"; _chkRecText.Checked = true; _chkRecText.AutoSize = true; _chkRecText.Margin = new Padding(12, 7, 0, 0); _numRecDelay.Minimum = 200; _numRecDelay.Maximum = 60000; _numRecDelay.Value = 2000; _numRecDelay.Increment = 500; _numRecDelay.Width = 76; _numRecDelay.Margin = new Padding(10, 6, 0, 0); optBar.Controls.AddRange(new Control[] { _chkRecMove, _chkRecText, new Label { Text = "最大延时(ms)", AutoSize = true, Margin = new Padding(12, 9, 0, 0) }, _numRecDelay }); var recBottom = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 38, WrapContents = false }; recBottom.Controls.Add(MakeButton("填入当前任务 →", (s, e) => { if (_txtRecPreview.Text.Length == 0) { _log.Write("还没有录制结果"); return; } _editor.Script = _txtRecPreview.Text; _detailTabs.SelectedIndex = 0; _log.Write("录制脚本已填入当前任务"); }, C_Blue)); recBottom.Controls.Add(MakeButton("▶ 直接运行", (s, e) => { if (_txtRecPreview.Text.Length > 0) _engine.Run(_txtRecPreview.Text); }, C_Green)); recBottom.Controls.Add(MakeButton("清空", (s, e) => _txtRecPreview.Clear())); _txtRecPreview = new TextBox { Dock = DockStyle.Fill, Multiline = true, ScrollBars = ScrollBars.Both, WordWrap = false, Font = new Font("Consolas", 10F), BackColor = Color.FromArgb(252, 253, 255), BorderStyle = BorderStyle.FixedSingle }; gbRec.Controls.Add(_txtRecPreview); gbRec.Controls.Add(recBottom); gbRec.Controls.Add(optBar); gbRec.Controls.Add(recBar); // ---- 屏幕录像 ---- var gbCap = new GroupBox { Text = " 屏幕录像(AVI / MJPG)", Dock = DockStyle.Fill, Padding = new Padding(8), Font = new Font("Microsoft YaHei UI", 9F) }; var capBar = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 40, WrapContents = false }; capBar.Controls.Add(MakeButton("全屏录像", (s, e) => StartCapture(null), C_Red)); capBar.Controls.Add(MakeButton("框选区域", (s, e) => PickCaptureRegion())); capBar.Controls.Add(MakeButton("停止录像", (s, e) => { _screenRec.Stop(); UpdateCapStatus(); })); var capOpt = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 34, WrapContents = false }; _numFps.Minimum = 1; _numFps.Maximum = 30; _numFps.Value = 15; _numFps.Width = 56; _numFps.Margin = new Padding(6, 6, 0, 0); _chkCursor.Text = "录鼠标指针"; _chkCursor.Checked = true; _chkCursor.AutoSize = true; _chkCursor.Margin = new Padding(12, 7, 0, 0); capOpt.Controls.AddRange(new Control[] { new Label { Text = "帧率", AutoSize = true, Margin = new Padding(4, 9, 0, 0) }, _numFps, _chkCursor }); _lblCapState = new Label { Dock = DockStyle.Fill, Text = "空闲。录像保存在程序目录 recordings/ 下。", ForeColor = C_Sub, Padding = new Padding(4, 8, 0, 0) }; gbCap.Controls.Add(_lblCapState); gbCap.Controls.Add(capOpt); gbCap.Controls.Add(capBar); split.Panel1.Controls.Add(gbRec); split.Panel2.Controls.Add(gbCap); page.Controls.Add(split); _screenRec.Finished += (path, frames) => { _log.Write($"录屏完成:{path}({frames} 帧)"); BeginInvoke(UpdateCapStatus); }; return page; } private TabPage BuildBrowserPage() { var page = new TabPage("内嵌浏览器"); var bar = new Panel { Dock = DockStyle.Top, Height = 38, BackColor = C_Bg }; var btnBack = MiniNav("←", 6); var btnFwd = MiniNav("→", 44); var btnReload = MiniNav("⟳", 82); var btnGo = new Button { Text = "转到", Left = 0, Top = 6, Width = 68, Height = 26, Anchor = AnchorStyles.Top | AnchorStyles.Right }; _txtUrl.Left = 124; _txtUrl.Top = 7; _txtUrl.Width = 600; _txtUrl.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; _txtUrl.Font = new Font("Microsoft YaHei UI", 9.5F); bar.Layout += (s, e) => { btnGo.Left = Math.Max(200, bar.Width - 78); _txtUrl.Width = Math.Max(120, btnGo.Left - 132); }; btnBack.Click += (s, e) => { if (_web.CoreWebView2?.CanGoBack == true) _web.CoreWebView2.GoBack(); }; btnFwd.Click += (s, e) => { if (_web.CoreWebView2?.CanGoForward == true) _web.CoreWebView2.GoForward(); }; btnReload.Click += (s, e) => _web.CoreWebView2?.Reload(); btnGo.Click += (s, e) => NavigateUrl(); _txtUrl.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) NavigateUrl(); }; _web = new WebView2 { Dock = DockStyle.Fill }; bar.Controls.AddRange(new Control[] { btnBack, btnFwd, btnReload, _txtUrl, btnGo }); page.Controls.Add(_web); page.Controls.Add(bar); return page; } private Button MiniNav(string text, int left) => new() { Text = text, Left = left, Width = 34, Top = 6, Height = 26, Font = new Font("Segoe UI", 10F, FontStyle.Bold) }; // ===================== 右:插件 ===================== private Control BuildPluginPane() { var wrap = new Panel { Dock = DockStyle.Fill, BackColor = C_Panel }; wrap.Controls.Add(PaneHeader("插件", DockStyle.Top)); wrap.Controls.Add(_pluginPanel); wrap.Controls.Add(new Label { Dock = DockStyle.Bottom, Height = 24, Text = "插件面板 · 点击直接调用", TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(10, 0, 0, 0), Font = _fSub, ForeColor = C_Sub, BackColor = C_Head }); _pluginPanel.Dock = DockStyle.Fill; _pluginPanel.FlowDirection = FlowDirection.TopDown; _pluginPanel.WrapContents = false; _pluginPanel.AutoScroll = true; _pluginPanel.BackColor = C_Panel; _pluginPanel.Padding = new Padding(0, 4, 0, 4); PluginGroup("屏幕与定位", Color.FromArgb(139, 92, 246)); PluginItem("坐标取色器", OpenPicker); PluginItem("框选截图 (F11)", TakeClipShot); PluginItem("全屏截图", () => { ScreenShot("images/shot_" + DateTime.Now.ToString("HHmmss") + ".png"); }); PluginItem("找图测试…", TestFindImage); PluginItem("打开图库目录", () => { string dir = Path.Combine(AppContext.BaseDirectory, "images"); Directory.CreateDirectory(dir); Process.Start(new ProcessStartInfo("explorer.exe", dir) { UseShellExecute = true }); }); PluginGroup("录制", Color.FromArgb(226, 104, 72)); PluginItem("开始/停止键鼠录制", ToggleRecord); PluginItem("全屏录屏", () => StartCapture(null)); PluginItem("框选区域录屏", PickCaptureRegion); PluginItem("停止录屏", () => { _screenRec.Stop(); UpdateCapStatus(); }); PluginItem("打开录像目录", () => { string dir = Path.Combine(AppContext.BaseDirectory, "recordings"); Directory.CreateDirectory(dir); Process.Start(new ProcessStartInfo("explorer.exe", dir) { UseShellExecute = true }); }); PluginGroup("脚本", Color.FromArgb(59, 130, 246)); PluginItem("运行当前任务", RunSelectedTask); PluginItem("积木 → 脚本", BlocksToLua); PluginItem("脚本 → 积木", () => { _detailTabs.SelectedIndex = 1; SyncScriptToBlocks(); }); PluginItem("打开脚本…", OpenLua); PluginItem("载入积木…", OpenBlocks); PluginGroup("系统与帮助", Color.FromArgb(100, 116, 139)); PluginItem("窗口列表", () => { var ws = _windows.GetWindows(); _log.Write($"当前可见窗口 {ws.Count} 个:"); foreach (var w in ws.Take(40)) _log.Write($" · {w.Title} [0x{w.Handle.ToInt64():X}]"); }); PluginItem("Lua 语法速查", ShowHelp); PluginItem("关于", () => MessageBox.Show(this, "按键精灵 · 全 C# 实现\n\nLua 引擎 MoonSharp / 自绘积木 / 键鼠录制 / AVI 录屏", "关于", MessageBoxButtons.OK, MessageBoxIcon.Information)); return wrap; } private void PluginGroup(string title, Color color) { _pluginPanel.Controls.Add(new Label { Text = "▍ " + title, Width = 186, Height = 26, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(8, 6, 0, 0), Margin = new Padding(6, 8, 6, 2), Font = new Font("Microsoft YaHei UI", 8.5F, FontStyle.Bold), ForeColor = color }); } private void PluginItem(string text, Action onClick) { var b = new Button { Text = " " + text, Width = 186, Height = 30, Margin = new Padding(6, 2, 6, 2), FlatStyle = FlatStyle.Flat, TextAlign = ContentAlignment.MiddleLeft, BackColor = Color.White, ForeColor = Color.FromArgb(45, 55, 72), Cursor = Cursors.Hand, Font = new Font("Microsoft YaHei UI", 9F) }; b.FlatAppearance.BorderColor = Color.FromArgb(224, 229, 238); b.FlatAppearance.BorderSize = 1; b.MouseEnter += (s, e) => { b.BackColor = Color.FromArgb(232, 240, 254); b.FlatAppearance.BorderColor = Color.FromArgb(160, 190, 240); }; b.MouseLeave += (s, e) => { b.BackColor = Color.White; b.FlatAppearance.BorderColor = Color.FromArgb(224, 229, 238); }; b.Click += (s, e) => { try { onClick(); } catch (Exception ex) { _log.Write("插件执行失败:" + ex.Message); } }; _pluginPanel.Controls.Add(b); } // ===================== 下:日志 ===================== private Control BuildLogPane() { var wrap = new Panel { Dock = DockStyle.Fill, BackColor = C_Panel }; var head = new Panel { Dock = DockStyle.Top, Height = 30, BackColor = C_Head }; head.Controls.Add(new Label { Text = "运行日志", Dock = DockStyle.Left, Width = 120, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(10, 0, 0, 0), Font = _fHead, ForeColor = C_HeadText }); var btnClear = new Button { Text = "清空", Dock = DockStyle.Right, Width = 56, FlatStyle = FlatStyle.Flat, BackColor = C_Head, ForeColor = C_HeadText, Font = _fSub, Cursor = Cursors.Hand }; btnClear.FlatAppearance.BorderSize = 0; btnClear.Click += (s, e) => _logBox.Clear(); head.Controls.Add(btnClear); _logBox.Dock = DockStyle.Fill; _logBox.Multiline = true; _logBox.ReadOnly = true; _logBox.ScrollBars = RichTextBoxScrollBars.Vertical; _logBox.BackColor = Color.FromArgb(28, 32, 40); _logBox.ForeColor = Color.FromArgb(214, 222, 235); _logBox.BorderStyle = BorderStyle.None; _logBox.Font = new Font("Consolas", 9.5F); _logBox.WordWrap = false; _logBox.DetectUrls = false; wrap.Controls.Add(_logBox); wrap.Controls.Add(head); return wrap; } private void FlushLog() { var lines = _log.Drain(); if (lines.Count == 0) return; _logBox.SuspendLayout(); foreach (var line in lines) { _logBox.SelectionStart = _logBox.TextLength; _logBox.SelectionLength = 0; _logBox.SelectionColor = line.Contains("失败") || line.Contains("错误") || line.Contains("✘") ? Color.FromArgb(255, 138, 128) : line.Contains("✔") || line.Contains("完成") || line.Contains("已保存") ? Color.FromArgb(140, 225, 160) : Color.FromArgb(214, 222, 235); _logBox.AppendText(line + Environment.NewLine); } if (_logBox.TextLength > 200_000) { _logBox.Select(_logBox.TextLength - 80_000, 80_000); _logBox.SelectedText = ""; } _logBox.SelectionStart = _logBox.TextLength; _logBox.ScrollToCaret(); _logBox.ResumeLayout(); } // ===================== 工具栏 / 状态栏 ===================== private Control BuildToolbar() { var bar = new Panel { Dock = DockStyle.Top, Height = 44, BackColor = C_Panel, Padding = new Padding(6, 6, 0, 0) }; var flow = new FlowLayoutPanel { Dock = DockStyle.Fill, WrapContents = false, BackColor = C_Panel }; _btnStart = MakeButton("▶ 启动 (F8)", (s, e) => StartQueue(), C_Green); _btnStart.Width = 108; _btnPause = MakeButton("‖ 暂停 (F9)", (s, e) => _engine.PauseOrResume()); _btnStop = MakeButton("■ 停止 (F10)", (s, e) => _engine.Stop(), C_Red); flow.Controls.Add(_btnStart); flow.Controls.Add(_btnPause); flow.Controls.Add(_btnStop); flow.Controls.Add(Sep()); flow.Controls.Add(MakeButton("+ 新建任务", (s, e) => AddTask(), C_Blue)); flow.Controls.Add(MakeButton("复制", (s, e) => DuplicateTask())); flow.Controls.Add(MakeButton("删除", (s, e) => DeleteTask())); flow.Controls.Add(Sep()); flow.Controls.Add(MakeButton("录制 (F12)", (s, e) => ToggleRecord())); flow.Controls.Add(MakeButton("截图 (F11)", (s, e) => TakeClipShot())); flow.Controls.Add(Sep()); _chkJitter.Text = "拟人化"; _chkJitter.Checked = true; _chkJitter.CheckedChanged += (s, e) => _jitter = _chkJitter.Checked; _chkJitter.AutoSize = true; _chkJitter.Margin = new Padding(4, 11, 0, 0); flow.Controls.Add(_chkJitter); bar.Controls.Add(flow); return bar; } private static Label Sep() => new() { Text = "│", AutoSize = true, ForeColor = Color.FromArgb(210, 216, 226), Margin = new Padding(8, 8, 8, 0) }; private Control BuildStatusBar() { var ss = new StatusStrip { BackColor = C_Head, SizingGrip = false }; _ssState.Text = "状态:空闲"; _ssState.Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold); _ssState.ForeColor = C_Green; _ssCaret.Text = "行 1, 列 1"; _ssCaret.ForeColor = C_Sub; _ssCaret.BorderSides = ToolStripStatusLabelBorderSides.Left; _ssCaret.BorderStyle = Border3DStyle.Etched; _ssHint.Text = "F8 启动 · F9 暂停 · F10 停止 · F11 截图 · F12 录制 · 鼠标到 (0,0) 急停"; _ssHint.ForeColor = C_Sub; _ssHint.Spring = true; _ssHint.TextAlign = ContentAlignment.MiddleRight; ss.Items.Add(_ssState); ss.Items.Add(_ssHint); ss.Items.Add(_ssCaret); return ss; } private static Control PaneHeader(string text, DockStyle dock) => new Label { Text = " " + text, Dock = dock, Height = 30, TextAlign = ContentAlignment.MiddleLeft, Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Bold), BackColor = C_Head, ForeColor = C_HeadText }; private static Button MakeButton(string text, EventHandler onClick, Color? back = null) { var b = new Button { Text = text, AutoSize = false, Height = 28, Width = Math.Max(78, text.Length * 13 + 16), Margin = new Padding(4, 3, 0, 0), FlatStyle = FlatStyle.Flat, Cursor = Cursors.Hand, Font = new Font("Microsoft YaHei UI", 9F) }; if (back.HasValue) { b.BackColor = back.Value; b.ForeColor = Color.White; b.FlatAppearance.BorderSize = 0; } else { b.BackColor = Color.White; b.FlatAppearance.BorderColor = Color.FromArgb(214, 220, 230); } b.Click += onClick; return b; } // ===================== 任务增删改 ===================== private void AddTask() { var t = new LuaTask { Name = $"任务 {_tasks.Count + 1}", Script = NewTaskScript, Source = "新建", LoopCount = 1, IntervalMs = 1000 }; _tasks.Add(t); _taskList.SelectedItem = t; _txtName.Focus(); _txtName.SelectAll(); _log.Write("已新建任务:" + t.Name); } private void DuplicateTask() { if (_current == null) return; var t = new LuaTask { Name = _current.Name + " 副本", Enabled = _current.Enabled, Infinite = _current.Infinite, LoopCount = _current.LoopCount, IntervalMs = _current.IntervalMs, Script = _current.Script, Hotkey = _current.Hotkey, Source = _current.Source }; int i = _tasks.IndexOf(_current); _tasks.Insert(i + 1, t); _taskList.SelectedItem = t; _log.Write("已复制任务:" + t.Name); } private void DeleteTask() { if (_current == null) return; string name = _current.Name; int i = _tasks.IndexOf(_current); _current = null; _tasks.RemoveAt(i); if (_tasks.Count > 0) _taskList.SelectedIndex = Math.Min(i, _tasks.Count - 1); else LoadTaskDetail(null); _log.Write("已删除任务:" + name); } private void MoveTask(int dir) { if (_current == null) return; int i = _tasks.IndexOf(_current), j = i + dir; if (j < 0 || j >= _tasks.Count) return; _tasks.RemoveAt(i); _tasks.Insert(j, _current); _taskList.SelectedItem = _current; } private void SelectTask(LuaTask? t) { _current = t; LoadTaskDetail(t); _taskList.Invalidate(); } /// 任务集合变动后重建列表项(手工同步,避免 DataSource 延迟绑定)。 private void RefreshTaskList() { bool prev = _sync; _sync = true; try { _taskList.BeginUpdate(); _taskList.Items.Clear(); foreach (var t in _tasks) _taskList.Items.Add(t); if (_current != null && _tasks.Contains(_current)) _taskList.SelectedItem = _current; else _taskList.ClearSelected(); _taskList.EndUpdate(); } finally { _sync = prev; } _taskList.Invalidate(); } private void LoadTaskDetail(LuaTask? t) { _sync = true; _converting = true; try { bool has = t != null; _txtName.Enabled = has; _chkEnabled.Enabled = has; _chkInfinite.Enabled = has; _numLoop.Enabled = has && !(t?.Infinite ?? false); _numInterval.Enabled = has; _txtName.Text = t?.Name ?? ""; _chkEnabled.Checked = t?.Enabled ?? false; _chkInfinite.Checked = t?.Infinite ?? false; _cboHotkey.SelectedIndex = Math.Max(0, t?.Hotkey is { Length: > 0 } hk && _cboHotkey.Items.Contains(hk) ? _cboHotkey.Items.IndexOf(hk) : 0); _numLoop.Value = Math.Clamp(t?.LoopCount ?? 1, 1, 999999); _numInterval.Value = Math.Clamp(t?.IntervalMs ?? 1000, 0, 3600000); _lblSource.Text = t == null ? "未选中任务 · 在左侧任务列表点一个任务" : $"来源 {((string.IsNullOrEmpty(t.Source) ? "内置" : t.Source))} · 热键 {(string.IsNullOrEmpty(t.Hotkey) ? "未绑定" : t.Hotkey)}"; // 兼容旧数据:只有积木没有脚本的任务,先转成脚本 if (has && string.IsNullOrWhiteSpace(t!.Script) && !string.IsNullOrWhiteSpace(t.Blocks)) t.Script = BlockCatalog.ToLua(BlockCatalog.FromJson(t.Blocks)); _editor.Script = t?.Script ?? ""; _canvas.ClearAll(); // 积木在切到积木页时才从脚本解析 } finally { _converting = false; _sync = false; } // 任务载入后立刻把脚本解析成积木,画布始终与脚本一致 SyncScriptToBlocks(); } /// 脚本 → 积木:解析当前脚本到画布(进入积木页时调用)。 private void SyncScriptToBlocks() { if (_converting) return; _converting = true; try { _canvas.LoadBlocks(BlockCatalog.FromLua(_editor.Script)); _lblBlockInfo.Text = $"共 {CountBlocks(_canvas.Blocks)} 个积木(由脚本转换)"; } catch (Exception ex) { _log.Write("脚本转积木失败:" + ex.Message); } finally { _converting = false; } } private void RunSelectedTask() { if (_current == null) { _log.Write("请先在左侧选中一个任务"); return; } string code = _current.Script; if (string.IsNullOrWhiteSpace(code)) { _log.Write("任务脚本为空"); return; } var err = _engine.Validate(code); if (err != null) { _log.Write("语法错误:" + err); return; } _log.Write($"运行任务:{_current.Name}"); _engine.Run(code); } // ===================== 积木相关 ===================== private void BuildLibrary() { _lib.BeginUpdate(); _lib.Nodes.Clear(); foreach (var cat in BlockCatalog.Categories) { var node = new TreeNode(cat) { NodeFont = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold) }; foreach (var def in BlockCatalog.All.Where(d => d.Category == cat)) { var title = def.Title; for (int i = 0; i < def.Args.Length; i++) { var v = string.IsNullOrWhiteSpace(def.Args[i].Default) ? def.Args[i].Label : def.Args[i].Default; title = title.Replace("{" + i + "}", v); } var child = new TreeNode(title) { Tag = def.Key }; if (def.Help.Length > 0) child.ToolTipText = def.Help; node.Nodes.Add(child); } _lib.Nodes.Add(node); } _lib.ExpandAll(); _lib.EndUpdate(); } private void AddBlockFromLibrary(string key) { var def = BlockCatalog.Find(key); if (def == null) return; _canvas.AddBlock(new LuaBlock { Key = key }); _lblBlockInfo.Text = "已添加:" + def.Title; } private static int CountBlocks(IEnumerable blocks) { int n = 0; foreach (var b in blocks) { n++; n += CountBlocks(b.Children); n += CountBlocks(b.ElseChildren); } return n; } /// 右侧参数面板:选中积木时生成对应输入控件。 private void BuildParamPanel(LuaBlock? block) { _paramPanel.SuspendLayout(); _paramPanel.Controls.Clear(); if (block == null) { _paramPanel.Controls.Add(new Label { Text = "选中一个积木后\n在这里修改参数", Dock = DockStyle.Top, Height = 60, ForeColor = C_Sub, Padding = new Padding(4, 12, 0, 0) }); _paramPanel.ResumeLayout(); return; } var def = BlockCatalog.Find(block.Key); if (def == null) { _paramPanel.ResumeLayout(); return; } while (block.Values.Count < def.Args.Length) block.Values.Add(""); var stack = new FlowLayoutPanel { Dock = DockStyle.Top, FlowDirection = FlowDirection.TopDown, WrapContents = false, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink }; var head = new Label { Text = BlockCatalog.RenderTitle(block), AutoSize = false, Width = 224, Height = 46, Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold), ForeColor = Color.FromArgb(35, 45, 65) }; stack.Controls.Add(head); if (def.Help.Length > 0) { stack.Controls.Add(new Label { Text = def.Help, Width = 224, Height = 34, ForeColor = C_Sub, Font = new Font("Microsoft YaHei UI", 8F) }); } for (int i = 0; i < def.Args.Length; i++) { var arg = def.Args[i]; int idx = i; stack.Controls.Add(new Label { Text = arg.Label, AutoSize = true, ForeColor = Color.FromArgb(90, 105, 130), Margin = new Padding(3, 8, 3, 2) }); if (arg.Options is { Length: > 0 }) { var combo = new ComboBox { Width = 220, DropDownStyle = ComboBoxStyle.DropDownList, Font = new Font("Microsoft YaHei UI", 9F) }; combo.Items.AddRange(arg.Options); string cur = block.Value(idx); combo.SelectedItem = arg.Options.Contains(cur) ? cur : arg.Options[0]; combo.SelectedIndexChanged += (s, e) => { block.Values[idx] = combo.SelectedItem?.ToString() ?? ""; head.Text = BlockCatalog.RenderTitle(block); _canvas.NotifyChanged(); }; stack.Controls.Add(combo); } else { var tb = new TextBox { Width = 220, Text = block.Value(idx), Font = new Font("Consolas", 10F), BorderStyle = BorderStyle.FixedSingle }; tb.TextChanged += (s, e) => { block.Values[idx] = tb.Text; head.Text = BlockCatalog.RenderTitle(block); _canvas.NotifyChanged(); }; stack.Controls.Add(tb); } } if (def.Shape is BlockShape.If or BlockShape.IfExists) { var btnElse = MakeButton(block.HasElse ? "移除 否则 分支" : "添加 否则 分支", (s, e) => { _canvas.ToggleElse(); BuildParamPanel(block); }); btnElse.Margin = new Padding(3, 14, 3, 3); stack.Controls.Add(btnElse); } var btnDel = MakeButton("删除此积木", (s, e) => _canvas.RemoveSelected()); btnDel.Margin = new Padding(3, 6, 3, 3); stack.Controls.Add(btnDel); _paramPanel.Controls.Add(stack); _paramPanel.ResumeLayout(); } // ===================== 录制 / 录屏 ===================== private void UpdateCapStatus() { if (_lblCapState == null) return; if (_screenRec.IsRecording) { double sec = _screenRec.FrameCount / Math.Max(1.0, _screenRec.Fps); _lblCapState.Text = $"● 录制中… {_screenRec.FrameCount} 帧 / {sec:0.0} 秒 → {_screenRec.OutputPath}"; _lblCapState.ForeColor = C_Red; } else { _lblCapState.Text = _screenRec.OutputPath == null ? "空闲。录像保存在程序目录 recordings/ 下。" : $"已保存:{_screenRec.OutputPath}({_screenRec.FrameCount} 帧)"; _lblCapState.ForeColor = C_Sub; } } private void PickCaptureRegion() { using var overlay = new OverlayForm { SaveToFile = false }; if (overlay.ShowDialog(this) == DialogResult.OK && overlay.SelectedRect.Width > 16) { _capRegion = overlay.SelectedRect; StartCapture(_capRegion); } } private void StartCapture(Rectangle? region) { if (_screenRec.IsRecording) { _log.Write("正在录像中,请先停止"); return; } var rect = region ?? SystemInformation.VirtualScreen; string dir = Path.Combine(AppContext.BaseDirectory, "recordings"); string file = Path.Combine(dir, $"rec_{DateTime.Now:yyyyMMdd_HHmmss}.avi"); try { _screenRec.Fps = (int)_numFps.Value; _screenRec.DrawCursor = _chkCursor.Checked; _screenRec.Start(file, rect); _log.Write($"开始录屏:{rect.Width}x{rect.Height} @ {_screenRec.Fps}fps → {file}"); UpdateCapStatus(); } catch (Exception ex) { _log.Write("录屏启动失败:" + ex.Message); } } private void ToggleRecord() { if (_recorder.IsRecording) { string code = _recorder.Stop(); _txtRecPreview.Text = code; _lblRecState.Text = "已停止,生成 " + code.Split('\n').Length + " 行"; _lblRecState.ForeColor = C_Green; _log.Write("录制结束,已生成 Lua 脚本"); } else { _recorder.RecordMouseMove = _chkRecMove.Checked; _recorder.SmartText = _chkRecText.Checked; _recorder.MaxDelayMs = (int)_numRecDelay.Value; _recorder.Start(); _log.Write("开始录制,按 F12 停止"); } } private void OnRecordState(bool recording) { BeginInvoke(() => { if (_btnRecToggle == null || _lblRecState == null) return; _btnRecToggle.Text = recording ? "■ 停止录制 (F12)" : "● 开始录制 (F12)"; _btnRecToggle.BackColor = recording ? Color.FromArgb(90, 100, 120) : C_Red; if (recording) { _lblRecState.Text = "● 录制中… 去操作目标程序"; _lblRecState.ForeColor = C_Red; } }); } // ===================== 插件动作 ===================== private void OpenPicker() { foreach (Form f in Application.OpenForms) if (f is PickerForm pf) { pf.Activate(); return; } new PickerForm(_log).Show(this); } private void TestFindImage() { string imgDir = Path.Combine(AppContext.BaseDirectory, "images"); using var dlg = new OpenFileDialog { Filter = "图片 (*.png;*.jpg;*.bmp)|*.png;*.jpg;*.bmp|所有文件 (*.*)|*.*", InitialDirectory = Directory.Exists(imgDir) ? imgDir : AppContext.BaseDirectory }; if (dlg.ShowDialog(this) != DialogResult.OK) return; var vs = SystemInformation.VirtualScreen; var sw = Stopwatch.StartNew(); using var bmp = _capture.CaptureRegion(vs.Left, vs.Top, vs.Width, vs.Height); var r = _finder.Find(bmp, dlg.FileName, 0.85, vs.Left, vs.Top); sw.Stop(); if (r.X < 0) _log.Write($"找图测试:未找到({sw.ElapsedMilliseconds}ms)"); else _log.Write($"找图测试:命中 ({r.X}, {r.Y}),相似度 {r.Score:0.000},耗时 {sw.ElapsedMilliseconds}ms"); } // ===================== 热键 ===================== /// 把各任务的启动热键注册到系统(F1~F7)。 private void RebindTaskHotkeys() { if (!IsHandleCreated) return; for (int id = HK_TASK_BASE; id < HK_TASK_BASE + 24; id++) Win32.UnregisterHotKey(Handle, id); _taskHotkeys.Clear(); int n = 0; foreach (var t in _tasks) { if (string.IsNullOrWhiteSpace(t.Hotkey)) continue; if (Enum.TryParse(t.Hotkey, true, out var k) && k != Keys.None) { int id = HK_TASK_BASE + n++; Win32.RegisterHotKey(Handle, id, Win32.MOD_NOREPEAT, (uint)k); _taskHotkeys[id] = t; } } } private void RunTaskByHotkey(LuaTask t) { SelectTask(t); _detailTabs.SelectedIndex = 0; RunSelectedTask(); } private void RegisterHotkeys() { Win32.RegisterHotKey(Handle, HK_START, Win32.MOD_NOREPEAT, (uint)Keys.F8); Win32.RegisterHotKey(Handle, HK_PAUSE, Win32.MOD_NOREPEAT, (uint)Keys.F9); Win32.RegisterHotKey(Handle, HK_STOP, Win32.MOD_NOREPEAT, (uint)Keys.F10); Win32.RegisterHotKey(Handle, HK_SHOT, Win32.MOD_NOREPEAT, (uint)Keys.F11); Win32.RegisterHotKey(Handle, HK_REC, Win32.MOD_NOREPEAT, (uint)Keys.F12); } private void UnregisterHotkeys() { for (int id = HK_START; id <= HK_REC; id++) Win32.UnregisterHotKey(Handle, id); } protected override void WndProc(ref Message m) { if (m.Msg == Win32.WM_HOTKEY) { int id = m.WParam.ToInt32(); if (id >= HK_TASK_BASE) { if (_taskHotkeys.TryGetValue(id, out var t)) RunTaskByHotkey(t); } else { switch (id) { case HK_START: StartQueue(); break; case HK_PAUSE: _engine.PauseOrResume(); break; case HK_STOP: _engine.Stop(); break; case HK_SHOT: TakeClipShot(); break; case HK_REC: ToggleRecord(); break; } } } base.WndProc(ref m); } // ===================== 运行 ===================== private void RunEditorScript() { var err = _engine.Validate(_editor.Script); if (err != null) { _log.Write("语法错误:" + err); return; } _engine.Run(_editor.Script); } private void ValidateScript() { var err = _engine.Validate(_editor.Script); _log.Write(err == null ? "✔ 语法校验通过" : "✘ 语法错误:" + err); } private void StartQueue() { if (_engine.State != LuaState.Idle) { _log.Write("已有脚本在运行"); return; } if (!_tasks.Any(t => t.Enabled)) { _log.Write("没有勾选的任务"); return; } _engine.RunQueue(_tasks.ToList()); } private void BlocksToLua() { if (_canvas.Blocks.Count == 0) { _log.Write("画布上没有积木"); return; } _converting = true; try { _editor.Script = BlockCatalog.ToLua(_canvas.Blocks); } finally { _converting = false; } _detailTabs.SelectedIndex = 0; _log.Write($"积木已写回脚本({CountBlocks(_canvas.Blocks)} 个积木)"); } private void RunBlocks() { if (_canvas.Blocks.Count == 0) { _log.Write("画布上没有积木"); return; } string code = BlockCatalog.ToLua(_canvas.Blocks); var err = _engine.Validate(code); if (err != null) { _log.Write("积木生成的脚本有语法错误:" + err); return; } _engine.Run(code); } private void OnEngineState(LuaState state) { BeginInvoke(() => { string text = state switch { LuaState.Running => "运行中", LuaState.Paused => "已暂停", _ => "空闲" }; _ssState.Text = "状态:" + text; _ssState.ForeColor = state switch { LuaState.Running => C_Blue, LuaState.Paused => Color.FromArgb(200, 140, 40), _ => C_Green }; if (_btnStart != null) _btnStart.Enabled = state == LuaState.Idle; }); } // ===================== 文件操作 ===================== private void OpenLua() { using var dlg = new OpenFileDialog { Filter = "Lua 脚本 (*.lua)|*.lua|文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*" }; if (dlg.ShowDialog(this) != DialogResult.OK) return; _editor.Script = File.ReadAllText(dlg.FileName); _luaPath = dlg.FileName; _detailTabs.SelectedIndex = 0; _log.Write("已打开脚本:" + dlg.FileName); } private void SaveLua(bool saveAs) { if (saveAs || _luaPath == null) { using var dlg = new SaveFileDialog { Filter = "Lua 脚本 (*.lua)|*.lua", FileName = (_current?.Name ?? "script") + ".lua" }; if (dlg.ShowDialog(this) != DialogResult.OK) return; _luaPath = dlg.FileName; } File.WriteAllText(_luaPath, _editor.Script); _log.Write("脚本已保存:" + _luaPath); } private void OpenBlocks() { using var dlg = new OpenFileDialog { Filter = "积木文件 (*.ajb)|*.ajb|所有文件 (*.*)|*.*" }; if (dlg.ShowDialog(this) != DialogResult.OK) return; try { var blocks = BlockCatalog.FromJson(File.ReadAllText(dlg.FileName)); _converting = true; try { _canvas.LoadBlocks(blocks); _editor.Script = BlockCatalog.ToLua(blocks); } finally { _converting = false; } _blockPath = dlg.FileName; _detailTabs.SelectedIndex = 1; _log.Write("已载入积木并转成脚本:" + dlg.FileName); } catch (Exception ex) { _log.Write("积木文件读取失败:" + ex.Message); } } private void SaveBlocks(bool saveAs) { if (saveAs || _blockPath == null) { using var dlg = new SaveFileDialog { Filter = "积木文件 (*.ajb)|*.ajb", FileName = (_current?.Name ?? "blocks") + ".ajb" }; if (dlg.ShowDialog(this) != DialogResult.OK) return; _blockPath = dlg.FileName; } File.WriteAllText(_blockPath, BlockCatalog.ToJson(_canvas.Blocks)); _log.Write("积木已保存:" + _blockPath); } private void OpenTaskList() { using var dlg = new OpenFileDialog { Filter = "任务列表 (*.ajt)|*.ajt|所有文件 (*.*)|*.*" }; if (dlg.ShowDialog(this) != DialogResult.OK) return; _current = null; _tasks.RaiseListChangedEvents = false; _tasks.Clear(); foreach (var t in LuaTaskFile.FromJson(File.ReadAllText(dlg.FileName))) _tasks.Add(t); _tasks.RaiseListChangedEvents = true; _tasks.ResetBindings(); _taskPath = dlg.FileName; if (_tasks.Count > 0) _taskList.SelectedIndex = 0; else LoadTaskDetail(null); _log.Write("已打开任务列表:" + dlg.FileName); } private void SaveTaskList() { if (_taskPath == null) { using var dlg = new SaveFileDialog { Filter = "任务列表 (*.ajt)|*.ajt", FileName = "tasks.ajt" }; if (dlg.ShowDialog(this) != DialogResult.OK) return; _taskPath = dlg.FileName; } File.WriteAllText(_taskPath, LuaTaskFile.ToJson(_tasks)); _log.Write("任务列表已保存:" + _taskPath); } private void TakeClipShot() { using var overlay = new OverlayForm(); overlay.OnSaved += f => _log.Write("截图已保存(可直接当模板图用):" + f); overlay.ShowDialog(this); } private void ShowHelp() { MessageBox.Show(this, HelpText, "Lua 语法速查", MessageBoxButtons.OK, MessageBoxIcon.Information); } // ===================== 浏览器 ===================== private Task EnsureWeb() { _webInit ??= InitWeb(); return _webInit; } private async Task InitWeb() { string dataDir = Path.Combine(AppContext.BaseDirectory, "webview2_data"); var env = await CoreWebView2Environment.CreateAsync(null, dataDir); await _web.EnsureCoreWebView2Async(env); _bridge = new WebViewBridge(_web); _web.CoreWebView2.Navigate("about:blank"); _web.CoreWebView2.SourceChanged += (s, e) => BeginInvoke(() => _txtUrl.Text = _web.Source?.AbsoluteUri ?? ""); } private void NavigateUrl() { string url = _txtUrl.Text.Trim(); if (url.Length == 0) return; if (!url.StartsWith("http") && !url.StartsWith("about:")) url = "https://" + url; EnsureWeb().ContinueWith(_ => BeginInvoke(() => _web.CoreWebView2?.Navigate(url))); } // ===================== ILuaHost ===================== public void Log(string msg) => _log.Write(msg); public bool JitterEnabled => _jitter; private string ResolvePath(string p) => Path.IsPathRooted(p) ? p : Path.Combine(AppContext.BaseDirectory, p); // ---- 鼠标 ---- void ILuaHost.MouseMove(int x, int y) => _mouse.MoveTo(x, y); void ILuaHost.MouseMoveR(int dx, int dy) => _mouse.MoveR(dx, dy); void ILuaHost.MouseClick(string button, int? x, int? y) { var b = button.ToLowerInvariant() switch { "right" => MouseButton.Right, "middle" => MouseButton.Middle, _ => MouseButton.Left }; _mouse.Click(b, x, y, JitterEnabled && x.HasValue); } void ILuaHost.MouseDoubleClick(int? x, int? y) => _mouse.DoubleClick(x, y, JitterEnabled && x.HasValue); void ILuaHost.MouseDown(string button) => _mouse.ButtonDown(ToBtn(button)); void ILuaHost.MouseUp(string button) => _mouse.ButtonUp(ToBtn(button)); void ILuaHost.MouseWheel(int delta) => _mouse.Wheel(delta); void ILuaHost.MouseDrag(int x1, int y1, int x2, int y2, int ms) => _mouse.Drag(x1, y1, x2, y2, ms); (int X, int Y) ILuaHost.MousePosition() { var p = _mouse.Position; return (p.X, p.Y); } private static MouseButton ToBtn(string b) => b.ToLowerInvariant() switch { "right" => MouseButton.Right, "middle" => MouseButton.Middle, _ => MouseButton.Left }; // ---- 键盘 ---- void ILuaHost.KeyPress(string key) => _keyboard.KeyPress(key); void ILuaHost.KeyDown(string key) => _keyboard.KeyDown(key); void ILuaHost.KeyUp(string key) => _keyboard.KeyUp(key); void ILuaHost.HotKey(string expr) => _keyboard.HotKey(expr); void ILuaHost.TypeText(string text) => _keyboard.TypeText(text); // ---- 屏幕 / 视觉 ---- public (int W, int H) ScreenSize() { var vs = SystemInformation.VirtualScreen; return (vs.Width, vs.Height); } public string ColorAt(int x, int y) { using var bmp = _capture.CaptureRegion(x, y, 1, 1); var c = bmp.GetPixel(0, 0); return $"{c.R:X2}{c.G:X2}{c.B:X2}"; } public (int X, int Y) FindColor(int x1, int y1, int x2, int y2, string hex, int tolerance) { using var bmp = _capture.CaptureRegion(x1, y1, Math.Max(1, x2 - x1 + 1), Math.Max(1, y2 - y1 + 1)); var p = _colorFinder.FindColor(bmp, 0, 0, bmp.Width - 1, bmp.Height - 1, ColorFinder.ParseHex(hex), tolerance); return p.X < 0 ? (-1, -1) : (p.X + x1, p.Y + y1); } public (int X, int Y, double Score) FindImage(int x1, int y1, int x2, int y2, string templatePath, double similarity) { string path = ResolvePath(templatePath); if (!File.Exists(path)) throw new FileNotFoundException($"模板图片不存在:{path}"); using var bmp = _capture.CaptureRegion(x1, y1, Math.Max(1, x2 - x1 + 1), Math.Max(1, y2 - y1 + 1)); return _finder.Find(bmp, path, similarity, x1, y1); } public void ScreenShot(string path) { path = ResolvePath(path); Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var bmp = _capture.CaptureScreen(); bmp.Save(path, System.Drawing.Imaging.ImageFormat.Png); _log.Write("截图已保存:" + path); } public string? PickRegion(string? savePath) { string? result = null; Invoke(() => { using var overlay = new OverlayForm(); overlay.OnSaved += f => result = f; overlay.ShowDialog(this); if (result == null && overlay.SavedPath != null) result = overlay.SavedPath; }); return result; } // ---- 窗口 / 系统 ---- public void Launch(string path, string? arguments) => _windows.Launch(path, arguments); public bool ActivateWindow(string titleKeyword) { bool ok = _windows.ActivateByTitle(titleKeyword); _log.Write(ok ? "已激活窗口:" + titleKeyword : "未找到窗口:" + titleKeyword); return ok; } public string[] ListWindows() => _windows.GetWindows().Select(w => w.Title).ToArray(); // ---- 剪贴板 ---- public string ClipGet() { string s = ""; Invoke(() => { try { s = Clipboard.ContainsText() ? Clipboard.GetText() : ""; } catch { } }); return s; } public void ClipSet(string text) => Invoke(() => { try { Clipboard.SetText(text); } catch { } }); // ---- 浏览器 ---- public void BrowserOpen(string url) { EnsureWeb().Wait(30000); _bridge!.Open(url); _log.Write("浏览器已打开:" + url); } public bool BrowserClick(string sel) { EnsureWeb().Wait(30000); return _bridge!.Click(sel); } public bool BrowserType(string sel, string text) { EnsureWeb().Wait(30000); return _bridge!.Type(sel, text); } public bool BrowserWaitFor(string sel, int ms) { EnsureWeb().Wait(30000); return _bridge!.WaitFor(sel, ms); } public string BrowserGetText(string sel) { EnsureWeb().Wait(30000); return _bridge!.GetText(sel); } public string BrowserExecJs(string js) { EnsureWeb().Wait(30000); return _bridge!.ExecJs(js); } public void BrowserShot(string path) { EnsureWeb().Wait(30000); _bridge!.Shot(ResolvePath(path)); } public void BrowserBack() { EnsureWeb().Wait(30000); BeginInvoke(() => { if (_web.CoreWebView2?.CanGoBack == true) _web.CoreWebView2.GoBack(); }); } public void BrowserForward() { EnsureWeb().Wait(30000); BeginInvoke(() => { if (_web.CoreWebView2?.CanGoForward == true) _web.CoreWebView2.GoForward(); }); } public void BrowserReload() { EnsureWeb().Wait(30000); BeginInvoke(() => _web.CoreWebView2?.Reload()); } // ===================== 内置文本 ===================== private const string NewTaskScript = """ -- 新任务 log("开始执行") delay(1000) log("执行完毕") """; private const string DefaultScript = """ -- 按键精灵 · Lua 脚本 -- 热键:F8 启动 / F9 暂停 / F10 停止 / F11 截图 / F12 录制操作 -- 鼠标甩到屏幕左上角 (0,0) 可紧急停止 -- -- 全局函数:delay / moveTo / leftClick / rightClick / doubleClick -- keyPress / hotkey / typeText / findImage / waitImage / exists -- screenshot / run / activate / log / clipSet / stop -- 命名空间:mouse.* key.* screen.* win.* browser.* clip.* sys.* log("开始执行") -- 打开记事本并输入一段中文(Unicode 注入,不受输入法影响) delay(800) run("notepad.exe") delay(1500) typeText("你好,这是按键精灵自动输入的文字") keyPress("ENTER") -- 循环按 5 次 A 键 for i = 1, 5 do keyPress("A") delay(200) end log("执行完毕") """; private const string HelpText = """ 【基础】 delay(毫秒) 等待 delayRandom(最小, 最大) 随机等待 log("文字") 输出日志 stop() 主动停止脚本 【鼠标】 moveTo(x, y) 移动到屏幕坐标 moveR(dx, dy) 相对移动 leftClick() / leftClick(x, y) rightClick() / rightClick(x, y) middleClick() doubleClick(x, y) wheel(-1) 滚轮(正上负下) drag(x1, y1, x2, y2, 毫秒) mouse.down("left") / mouse.up("left") local x, y = mouse.pos() 【键盘】 keyPress("ENTER") 单键:A~Z 0~9 F1~F12 ENTER SPACE TAB ESC LEFT UP RIGHT DOWN … hotkey("Ctrl+A") 组合键 typeText("中文文本") 整段文本注入 keyDown("SHIFT") / keyUp("SHIFT") 【找图找色】 local x, y = findImage("images/btn.png", 0.85) local x, y = findColor("FF0000", 10) local x, y = waitImage("images/btn.png", 10000) if exists("images/btn.png") then … end screenshot("shot.png") screen.pick("images/btn.png") 运行时框选截图存成模板 【系统】 run("notepad.exe") activate("记事本") clipSet("内容") local s = clipGet() 【浏览器】 browser.open("https://…") browser.click("#btn") browser.type("#input", "内容") browser.wait(".loaded", 10000) local t = browser.text(".result") browser.js("document.title") 【流程】 if 条件 then … elseif … else … end for i = 1, 10 do … end while 条件 do … end break """; } }