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
""";
}
}