Files
anjianjingling/Forms/MainForm.cs
T
2026-10-24 13:02:19 +08:00

1996 lines
84 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 主窗体。布局:左侧任务列表 / 中间任务详情 / 右侧插件 / 下方日志。
/// 全 C# 实现:Lua(MoonSharp)+ 自绘积木 + 键鼠录制 + 屏幕录像。
/// </summary>
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<LuaTask> _tasks = new();
private readonly ListBox _taskList = new();
private LuaTask? _current;
private bool _sync;
/// <summary>true = 正在脚本⇄积木互相灌入,抑制联动避免死循环。</summary>
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<int, LuaTask> _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<MenuStrip>().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();
}
/// <summary>任务集合变动后重建列表项(手工同步,避免 DataSource 延迟绑定)。</summary>
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();
}
/// <summary>脚本 → 积木:解析当前脚本到画布(进入积木页时调用)。</summary>
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<LuaBlock> blocks)
{
int n = 0;
foreach (var b in blocks)
{
n++;
n += CountBlocks(b.Children);
n += CountBlocks(b.ElseChildren);
}
return n;
}
/// <summary>右侧参数面板:选中积木时生成对应输入控件。</summary>
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");
}
// ===================== 热键 =====================
/// <summary>把各任务的启动热键注册到系统(F1~F7)。</summary>
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<Keys>(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
""";
}
}