1996 lines
84 KiB
C#
1996 lines
84 KiB
C#
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
|
||
""";
|
||
}
|
||
}
|