first commit

This commit is contained in:
hero920103 committed 2026-10-24 13:02:19 +08:00
commit 761b95080f
404 files changed
+312608

No files matched your search

+772
View File
@@ -0,0 +1,772 @@
using System.Drawing.Drawing2D;
using anjianjingling.Lua;
namespace anjianjingling.Forms
{
/// <summary>
/// 纯 WinForms 自绘积木画布:插片耦合(榫卯)造型。
/// 顶部凹槽 + 底部凸榫互相咬合;容器块为 C 形插槽;参数显示为可插入的白色插槽。
/// </summary>
public sealed class BlockCanvas : Panel
{
private enum RowKind { Block, Slot, Else, End }
private sealed class Row
{
public LuaBlock Block = null!;
public int Depth;
public Rectangle Bounds;
public List<LuaBlock> Owner = null!;
public int Index;
public RowKind Kind;
}
// ---- 几何(像素)----
private const int RowH = 30; // 语句块高度
private const int NotchH = 7; // 榫高:凸榫伸出 / 凹槽凹进
private const int NotchX = 26; // 榫相对块左边的起始位置
private const int NotchSlope = 7; // 斜边宽
private const int NotchFlat = 12; // 平台宽
private const int WallW = 22; // 容器左壁宽(子块缩进)
private const int FootH = 16; // 容器底部封口高
private const int ElseH = 22; // 否则分隔条高
private const int Radius = 4;
private const int MinInner = 24; // 空容器最小内腔高
private const int PadX = 12;
private const int PadTop = 12;
private readonly List<Row> _rows = new();
private LuaBlock? _selected;
private LuaBlock? _hover;
private Point _mouseDown;
private LuaBlock? _dragBlock;
private bool _dragging;
private (List<LuaBlock> List, int Index)? _dropTarget;
private int _dropY;
private int _dropX;
private static readonly Font TitleFont = new("Microsoft YaHei UI", 9.5F);
private static readonly Font SmallFont = new("Microsoft YaHei UI", 7.5F);
public List<LuaBlock> Blocks { get; } = new();
public event Action? Changed;
public event Action<LuaBlock?>? SelectionChanged;
public event Action<LuaBlock>? EditRequested;
public LuaBlock? SelectedBlock => _selected;
public BlockCanvas()
{
DoubleBuffered = true;
BackColor = Color.FromArgb(244, 247, 251);
AutoScroll = true;
AllowDrop = true;
SetStyle(ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint | ControlStyles.UserMouse, true);
}
// ===================== 拖入 =====================
public const string DragFormat = "aj_block_key";
protected override void OnDragEnter(DragEventArgs e)
{
if (e.Data?.GetDataPresent(DragFormat) == true) e.Effect = DragDropEffects.Copy;
base.OnDragEnter(e);
}
protected override void OnDragOver(DragEventArgs e)
{
if (e.Data?.GetDataPresent(DragFormat) == true)
{
e.Effect = DragDropEffects.Copy;
UpdateDropIndicator(PointToClient(new Point(e.X, e.Y)));
}
base.OnDragOver(e);
}
protected override void OnDragLeave(EventArgs e)
{
_dropTarget = null;
Invalidate();
base.OnDragLeave(e);
}
protected override void OnDragDrop(DragEventArgs e)
{
if (e.Data?.GetData(DragFormat) is string key && BlockCatalog.Find(key) != null)
{
UpdateDropIndicator(PointToClient(new Point(e.X, e.Y)));
var block = new LuaBlock { Key = key };
InsertAtDropTarget(block);
Select(block);
}
_dropTarget = null;
Invalidate();
base.OnDragDrop(e);
}
// ===================== 数据操作 =====================
public void AddBlock(LuaBlock block, bool atEnd = true)
{
if (atEnd || _selected == null) Blocks.Add(block);
else InsertAfter(_selected, block);
Select(block);
Invalidate();
Changed?.Invoke();
}
private void InsertAfter(LuaBlock anchor, LuaBlock block)
{
foreach (var (list, _) in EnumerateLists(Blocks))
{
int i = list.IndexOf(anchor);
if (i >= 0) { list.Insert(i + 1, block); return; }
}
Blocks.Add(block);
}
private static IEnumerable<(List<LuaBlock> List, int Depth)> EnumerateLists(List<LuaBlock> list, int depth = 0)
{
yield return (list, depth);
foreach (var b in list)
{
foreach (var x in EnumerateLists(b.Children, depth + 1)) yield return x;
foreach (var x in EnumerateLists(b.ElseChildren, depth + 1)) yield return x;
}
}
public void ClearAll()
{
Blocks.Clear();
_selected = null;
Invalidate();
Changed?.Invoke();
SelectionChanged?.Invoke(null);
}
/// <summary>内容有变化(含参数修改)时由外部触发重绘与变更通知。</summary>
public void NotifyChanged()
{
Invalidate();
Changed?.Invoke();
}
public void LoadBlocks(IEnumerable<LuaBlock> blocks)
{
Blocks.Clear();
Blocks.AddRange(blocks);
_selected = null;
AutoScrollPosition = Point.Empty; // 载入后回到顶部,避免旧滚动位置把第一块顶出视野
Invalidate();
Changed?.Invoke();
SelectionChanged?.Invoke(null);
}
public void Select(LuaBlock? b)
{
_selected = b;
SelectionChanged?.Invoke(b);
Invalidate();
}
public void RemoveSelected()
{
if (_selected == null) return;
RemoveFrom(Blocks, _selected);
_selected = null;
Invalidate();
Changed?.Invoke();
SelectionChanged?.Invoke(null);
}
public void DuplicateSelected()
{
if (_selected == null) return;
var copy = _selected.Clone();
InsertAfter(_selected, copy);
Select(copy);
Invalidate();
Changed?.Invoke();
}
public void MoveSelected(int dir)
{
if (_selected == null) return;
foreach (var (list, _) in EnumerateLists(Blocks))
{
int i = list.IndexOf(_selected);
if (i < 0) continue;
int j = i + dir;
if (j < 0 || j >= list.Count) return;
list.RemoveAt(i);
list.Insert(j, _selected);
Invalidate();
Changed?.Invoke();
return;
}
}
public void ToggleElse()
{
if (_selected == null) return;
var def = BlockCatalog.Find(_selected.Key);
if (def is not { Shape: BlockShape.If or BlockShape.IfExists }) return;
_selected.HasElse = !_selected.HasElse;
if (!_selected.HasElse) _selected.ElseChildren.Clear();
Invalidate();
Changed?.Invoke();
}
private static bool RemoveFrom(List<LuaBlock> list, LuaBlock target)
{
if (list.Remove(target)) return true;
foreach (var b in list)
{
if (RemoveFrom(b.Children, target)) return true;
if (RemoveFrom(b.ElseChildren, target)) return true;
}
return false;
}
private static bool IsListInside(LuaBlock root, List<LuaBlock> list)
{
foreach (var (l, _) in EnumerateLists(root.Children))
if (ReferenceEquals(l, list)) return true;
foreach (var (l, _) in EnumerateLists(root.ElseChildren))
if (ReferenceEquals(l, list)) return true;
return false;
}
// ===================== 鼠标 =====================
protected override void OnMouseDown(MouseEventArgs e)
{
Focus();
var p = ToLogical(e.Location);
var row = HitRow(p);
if (row == null || row.Kind is RowKind.Slot or RowKind.End)
{
// 空槽 / 封口行:不进入拖动,仅选中所属容器
Select(row?.Block);
base.OnMouseDown(e);
return;
}
if (e.Button == MouseButtons.Right)
{
Select(row.Block);
BuildMenu().Show(this, p);
return;
}
Select(row.Block);
_mouseDown = e.Location;
_dragBlock = row.Block;
_dragging = false;
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
var lp = ToLogical(e.Location);
var h = HitRow(lp);
var nb = h?.Block;
if (!ReferenceEquals(nb, _hover)) { _hover = nb; Cursor = nb == null ? Cursors.Default : Cursors.Hand; Invalidate(); }
if (_dragBlock != null && e.Button == MouseButtons.Left)
{
if (!_dragging && (Math.Abs(e.X - _mouseDown.X) > 6 || Math.Abs(e.Y - _mouseDown.Y) > 6))
_dragging = true;
if (_dragging)
{
Cursor = Cursors.SizeAll;
UpdateDropIndicator(lp);
}
}
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
if (_dragging && _dragBlock != null && _dropTarget is { } t)
{
var moving = _dragBlock;
if (!IsListInside(moving, t.List))
{
int oldIdx = t.List.IndexOf(moving);
RemoveFrom(Blocks, moving);
int idx = t.Index;
if (oldIdx >= 0 && oldIdx < idx) idx--;
idx = Math.Clamp(idx, 0, t.List.Count);
t.List.Insert(idx, moving);
Select(moving);
Changed?.Invoke();
}
}
_dragBlock = null;
_dragging = false;
_dropTarget = null;
Cursor = Cursors.Default;
Invalidate();
base.OnMouseUp(e);
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
var row = HitRow(ToLogical(e.Location));
if (row != null && row.Kind == RowKind.Block) EditRequested?.Invoke(row.Block);
base.OnMouseDoubleClick(e);
}
protected override void OnMouseLeave(EventArgs e)
{
if (_hover != null) { _hover = null; Invalidate(); }
base.OnMouseLeave(e);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (_selected != null)
{
switch (e.KeyCode)
{
case Keys.Delete: RemoveSelected(); e.Handled = true; break;
case Keys.Up when e.Control: MoveSelected(-1); e.Handled = true; break;
case Keys.Down when e.Control: MoveSelected(1); e.Handled = true; break;
}
}
base.OnKeyDown(e);
}
private ContextMenuStrip BuildMenu()
{
var m = new ContextMenuStrip { Font = new Font("Microsoft YaHei UI", 9F) };
m.Items.Add("编辑参数", null, (s, e) => { if (_selected != null) EditRequested?.Invoke(_selected); });
m.Items.Add("复制一份", null, (s, e) => DuplicateSelected());
m.Items.Add("上移", null, (s, e) => MoveSelected(-1));
m.Items.Add("下移", null, (s, e) => MoveSelected(1));
m.Items.Add(new ToolStripSeparator());
var def = _selected == null ? null : BlockCatalog.Find(_selected.Key);
if (def is { Shape: BlockShape.If or BlockShape.IfExists })
m.Items.Add(_selected!.HasElse ? "移除「否则」分支" : "添加「否则」分支", null, (s, e) => ToggleElse());
m.Items.Add("删除", null, (s, e) => RemoveSelected());
return m;
}
// ===================== 命中 / 插入点 =====================
private Point ToLogical(Point client)
=> new(client.X - AutoScrollPosition.X, client.Y - AutoScrollPosition.Y);
private Row? HitRow(Point p)
{
// 后画的在上面,倒序命中
for (int i = _rows.Count - 1; i >= 0; i--)
if (_rows[i].Bounds.Contains(p)) return _rows[i];
return null;
}
private void UpdateDropIndicator(Point p)
{
var hit = ComputeInsert(p);
_dropTarget = (hit.List, hit.Index);
_dropY = hit.Y;
_dropX = hit.X;
Invalidate();
}
private (List<LuaBlock> List, int Index, int Y, int X) ComputeInsert(Point p)
{
if (_rows.Count == 0) return (Blocks, 0, PadTop, PadX);
Row? hit = null;
foreach (var r in _rows)
if (p.Y >= r.Bounds.Top && p.Y <= r.Bounds.Bottom) { hit = r; break; }
if (hit == null)
{
for (int i = _rows.Count - 1; i >= 0; i--)
if (p.Y > _rows[i].Bounds.Bottom) { hit = _rows[i]; break; }
}
if (hit == null) return (Blocks, 0, PadTop, PadX);
int x = hit.Bounds.Left;
bool upper = p.Y < hit.Bounds.Top + hit.Bounds.Height / 2;
switch (hit.Kind)
{
case RowKind.Slot:
return (hit.Owner, hit.Index, hit.Bounds.Top + 2, x + WallW);
case RowKind.Else:
return upper ? (hit.Block.Children, hit.Block.Children.Count, hit.Bounds.Top + 1, x)
: (hit.Owner, hit.Index, hit.Bounds.Bottom + 1, x);
case RowKind.End:
return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left + WallW);
default:
if (upper) return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left);
var def = BlockCatalog.Find(hit.Block.Key);
if (def?.IsContainer == true)
return (hit.Block.Children, 0, hit.Bounds.Bottom + 2, hit.Bounds.Left + WallW);
return (hit.Owner, hit.Index + 1, hit.Bounds.Bottom + 1, hit.Bounds.Left);
}
}
private void InsertAtDropTarget(LuaBlock block)
{
if (_dropTarget is { } t)
{
int idx = Math.Clamp(t.Index, 0, t.List.Count);
t.List.Insert(idx, block);
}
else Blocks.Add(block);
Changed?.Invoke();
}
// ===================== 绘制 =====================
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y);
_rows.Clear();
int y = PadTop;
int w = Math.Max(200, ClientSize.Width - PadX - 20);
DrawList(g, Blocks, 0, ref y, PadX, w);
if (Blocks.Count == 0)
{
using var f = new Font("Microsoft YaHei UI", 10F);
using var b = new SolidBrush(Color.FromArgb(150, 160, 178));
g.DrawString("从左侧命令库双击或拖入积木,开始搭建流程", f, b, PadX + 8, PadTop + 6);
}
if (_dropTarget != null)
{
using var pen = new Pen(Color.FromArgb(37, 99, 235), 2);
int ww = Math.Max(140, ClientSize.Width - _dropX - 30);
g.DrawLine(pen, _dropX, _dropY, _dropX + ww, _dropY);
g.FillEllipse(Brushes.DodgerBlue, _dropX - 3, _dropY - 3, 6, 6);
}
int totalH = y + NotchH + 30;
if (AutoScrollMinSize.Height != totalH)
AutoScrollMinSize = new Size(0, totalH);
}
private static int MeasureList(List<LuaBlock> list)
{
int h = 0;
foreach (var b in list)
{
var def = BlockCatalog.Find(b.Key);
if (def == null) continue;
h += RowH;
if (def.IsContainer) h += InnerHeight(b, def) + FootH;
}
return h;
}
private static int InnerHeight(LuaBlock b, BlockDef def)
{
int h = MeasureList(b.Children);
if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse)
h += ElseH + MeasureList(b.ElseChildren);
return Math.Max(MinInner, h);
}
private void DrawList(Graphics g, List<LuaBlock> list, int depth, ref int y, int x, int w)
{
for (int i = 0; i < list.Count; i++)
{
var b = list[i];
var def = BlockCatalog.Find(b.Key);
if (def == null) continue;
bool sel = ReferenceEquals(b, _selected);
bool dim = _dragging && ReferenceEquals(b, _dragBlock);
var color = CategoryColor(def.Category);
if (!def.IsContainer)
{
var rect = new Rectangle(x, y, w, RowH);
using (var path = PuzzlePath(x, y, w, RowH, NotchX))
{
FillBlock(g, path, rect, color, sel, dim);
}
DrawLabel(g, b, def, x + 12, y + 7, w - 24, color);
_rows.Add(new Row { Block = b, Depth = depth, Bounds = rect, Owner = list, Index = i, Kind = RowKind.Block });
y += RowH;
continue;
}
// ---------- 容器块(C 形插槽)----------
int innerH = InnerHeight(b, def);
int headTop = y;
int innerTop = headTop + RowH;
int footTop = innerTop + innerH;
int footBottom = footTop + FootH;
using (var shell = ContainerPath(x, headTop, w, RowH, innerH, FootH, NotchX))
{
FillBlock(g, shell, new Rectangle(x, headTop, w, footBottom - headTop), color, sel, dim);
}
// 内腔插槽底
using (var innerBg = new SolidBrush(Color.FromArgb(233, 238, 246)))
g.FillRectangle(innerBg, x + WallW, innerTop, w - WallW, innerH);
using (var lipPen = new Pen(Darken(color, 0.18), 1f))
g.DrawRectangle(lipPen, x + WallW, innerTop, w - WallW - 1, innerH - 1);
DrawLabel(g, b, def, x + 12, headTop + 7, w - 24, color);
_rows.Add(new Row
{
Block = b, Depth = depth,
Bounds = new Rectangle(x, headTop, w, RowH),
Owner = list, Index = i, Kind = RowKind.Block
});
int cy = innerTop;
int iw = w - WallW;
if (b.Children.Count == 0)
{
_rows.Add(new Row
{
Block = b, Depth = depth + 1,
Bounds = new Rectangle(x + WallW, cy, iw, Math.Max(20, innerH - NotchH)),
Owner = b.Children, Index = 0, Kind = RowKind.Slot
});
DrawSlotHint(g, x + WallW + 8, cy + 6);
}
else
{
DrawList(g, b.Children, depth + 1, ref cy, x + WallW, iw);
}
var tailList = b.Children;
if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse)
{
int elseY = innerTop + NotchH + Math.Max(0, MeasureList(b.Children));
using var eb = new SolidBrush(Darken(color, 0.18));
g.FillRectangle(eb, x, elseY, w, ElseH);
using var ef = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
using var et = new SolidBrush(Color.FromArgb(240, 245, 252));
g.DrawString("否则", ef, et, x + WallW + 4, elseY + 3);
_rows.Add(new Row
{
Block = b, Depth = depth + 1,
Bounds = new Rectangle(x + WallW, elseY, iw, ElseH),
Owner = b.ElseChildren, Index = 0, Kind = RowKind.Else
});
int ey = elseY + ElseH;
if (b.ElseChildren.Count == 0)
{
_rows.Add(new Row
{
Block = b, Depth = depth + 1,
Bounds = new Rectangle(x + WallW, ey, iw, Math.Max(18, footTop - ey)),
Owner = b.ElseChildren, Index = 0, Kind = RowKind.Slot
});
DrawSlotHint(g, x + WallW + 8, ey + 4);
}
else
{
DrawList(g, b.ElseChildren, depth + 1, ref ey, x + WallW, iw);
}
tailList = b.ElseChildren;
}
_rows.Add(new Row
{
Block = b, Depth = depth,
Bounds = new Rectangle(x, footTop, w, FootH),
Owner = tailList, Index = tailList.Count, Kind = RowKind.End
});
y = footBottom;
}
}
private void FillBlock(Graphics g, GraphicsPath path, Rectangle r, Color color, bool sel, bool dim)
{
using (var fill = new LinearGradientBrush(
new Point(r.Left, r.Top), new Point(r.Left, r.Bottom),
Lighten(color, dim ? 0.42 : 0.16), dim ? Lighten(color, 0.42) : color))
{
g.FillPath(fill, path);
}
using var edge = new Pen(Darken(color, 0.32), 1f);
g.DrawPath(edge, path);
if (sel)
{
using var glow = new Pen(Color.FromArgb(250, 204, 21), 2.5f);
g.DrawPath(glow, path);
}
}
private static void DrawSlotHint(Graphics g, int x, int y)
{
using var f = new Font("Microsoft YaHei UI", 8F);
using var b = new SolidBrush(Color.FromArgb(158, 170, 190));
g.DrawString("把积木拖到这里", f, b, x, y);
}
// ---------- 标题(参数画成白色插槽)----------
private void DrawLabel(Graphics g, LuaBlock b, BlockDef def, int x, int y, int maxW, Color color)
{
float cx = x;
using var white = new SolidBrush(Color.White);
using var slotBg = new SolidBrush(Color.FromArgb(246, 248, 252));
using var slotFg = new SolidBrush(Color.FromArgb(48, 58, 74));
using var fmtC = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
foreach (var (text, isArg) in SplitTitle(def, b))
{
if (text.Length == 0) continue;
if (cx > x + maxW) break;
if (!isArg)
{
g.DrawString(text, TitleFont, white, cx, y);
cx += g.MeasureString(text, TitleFont).Width;
}
else
{
var sz = g.MeasureString(text, TitleFont);
float wpx = Math.Min(sz.Width + 14, Math.Max(48, maxW / 3));
var rect = new RectangleF(cx, y - 2f, wpx, 20);
using var path = Rounded(Rectangle.Round(rect), 5);
g.FillPath(slotBg, path);
using var pen = new Pen(Darken(color, 0.30), 1f);
g.DrawPath(pen, path);
var clip = new RectangleF(rect.Left + 2, rect.Top, rect.Width - 4, rect.Height);
g.DrawString(text, TitleFont, slotFg, clip, fmtC);
cx += wpx + 5;
}
}
}
private static List<(string Text, bool IsArg)> SplitTitle(BlockDef def, LuaBlock b)
{
var segs = new List<(string, bool)>();
if (def.Args.Length == 0) { segs.Add((def.Title, false)); return segs; }
int pos = 0;
for (int i = 0; i < def.Args.Length; i++)
{
string token = "{" + i + "}";
int idx = def.Title.IndexOf(token, StringComparison.Ordinal);
if (idx < 0 || idx < pos) continue;
if (idx > pos) segs.Add((def.Title[pos..idx], false));
segs.Add((b.Value(i), true));
pos = idx + token.Length;
}
if (pos < def.Title.Length) segs.Add((def.Title[pos..], false));
return segs;
}
// ---------- 路径:插片耦合造型 ----------
private static void TopEdge(GraphicsPath p, int x, int y, int w, int r, int nx)
{
p.AddLine(x + r, y, x + nx, y);
p.AddLine(x + nx, y, x + nx + NotchSlope, y + NotchH);
p.AddLine(x + nx + NotchSlope, y + NotchH, x + nx + NotchSlope + NotchFlat, y + NotchH);
p.AddLine(x + nx + NotchSlope + NotchFlat, y + NotchH, x + nx + 2 * NotchSlope + NotchFlat, y);
p.AddLine(x + nx + 2 * NotchSlope + NotchFlat, y, x + w - r, y);
p.AddArc(x + w - 2 * r, y, 2 * r, 2 * r, 270, 90);
}
private static void BottomEdge(GraphicsPath p, int x, int bottom, int w, int r, int nx)
{
int nEnd = x + nx + 2 * NotchSlope + NotchFlat;
p.AddLine(x + w - r, bottom, nEnd, bottom);
p.AddLine(nEnd, bottom, nEnd - NotchSlope, bottom + NotchH);
p.AddLine(nEnd - NotchSlope, bottom + NotchH, nEnd - NotchSlope - NotchFlat, bottom + NotchH);
p.AddLine(nEnd - NotchSlope - NotchFlat, bottom + NotchH, x + nx, bottom);
p.AddLine(x + nx, bottom, x + r, bottom);
p.AddArc(x, bottom - 2 * r, 2 * r, 2 * r, 90, 90);
}
/// <summary>单块:顶部凹槽 + 底部凸榫。</summary>
private static GraphicsPath PuzzlePath(int x, int y, int w, int h, int nx)
{
var p = new GraphicsPath();
int d = Radius * 2;
int bottom = y + h;
p.StartFigure();
p.AddArc(x, y, d, d, 180, 90); // 左上
TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上
p.AddLine(x + w, y + Radius, x + w, bottom - Radius); // 右
p.AddArc(x + w - d, bottom - d, d, d, 0, 90); // 右下
BottomEdge(p, x, bottom, w, Radius, nx); // 底边(凸榫)+ 左下
p.CloseFigure();
return p;
}
/// <summary>容器块:C 形,中间挖出插槽。</summary>
private static GraphicsPath ContainerPath(int x, int y, int w, int headH, int innerH, int footH, int nx)
{
var p = new GraphicsPath();
int d = Radius * 2;
int innerTop = y + headH;
int footTop = innerTop + innerH;
int footBottom = footTop + footH;
p.StartFigure();
p.AddArc(x, y, d, d, 180, 90); // 左上
TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上
p.AddLine(x + w, y + Radius, x + w, footBottom - Radius); // 右外墙
p.AddArc(x + w - d, footBottom - d, d, d, 0, 90); // 右下
BottomEdge(p, x, footBottom, w, Radius, nx); // 底边(凸榫)+ 左下
p.AddLine(x, footBottom - Radius, x, footTop + Radius); // 左外墙上返
p.AddArc(x, footTop, d, d, 90, 90); // 封口左上内角
p.AddLine(x + Radius, footTop, x + WallW, footTop); // 插槽底边
p.AddLine(x + WallW, footTop, x + WallW, innerTop); // 左壁内侧
p.AddLine(x + WallW, innerTop, x + Radius, innerTop); // 插槽顶边
p.AddArc(x, innerTop - d, d, d, 270, 90); // 插槽左上内角
p.CloseFigure();
return p;
}
private static GraphicsPath Rounded(Rectangle r, int radius)
{
var p = new GraphicsPath();
int d = radius * 2;
p.AddArc(r.Left, r.Top, d, d, 180, 90);
p.AddArc(r.Right - d, r.Top, d, d, 270, 90);
p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
p.AddArc(r.Left, r.Bottom - d, d, d, 90, 90);
p.CloseFigure();
return p;
}
// ---------- 颜色 ----------
private static Color CategoryColor(string category) => category switch
{
"流程" => Color.FromArgb(59, 130, 246),
"鼠标" => Color.FromArgb(16, 175, 122),
"键盘" => Color.FromArgb(214, 140, 18),
"视觉" => Color.FromArgb(139, 92, 246),
"系统" => Color.FromArgb(226, 104, 72),
"浏览器" => Color.FromArgb(14, 160, 226),
_ => Color.FromArgb(100, 116, 139)
};
private static Color Lighten(Color c, double f) => Color.FromArgb(
(int)Math.Min(255, c.R + 255 * f),
(int)Math.Min(255, c.G + 255 * f),
(int)Math.Min(255, c.B + 255 * f));
private static Color Darken(Color c, double f) => Color.FromArgb(
(int)(c.R * (1 - f)), (int)(c.G * (1 - f)), (int)(c.B * (1 - f)));
}
}
+202
View File
@@ -0,0 +1,202 @@
using System.Text.RegularExpressions;
using anjianjingling.Native;
namespace anjianjingling.Forms
{
/// <summary>
/// Lua 脚本编辑器:行号栏 + 语法着色(关键字/字符串/注释/数字)。
/// 纯 WinForms,无第三方依赖。
/// </summary>
public sealed class LuaEditor : UserControl
{
private readonly RichTextBox _rtb = new();
private readonly Panel _gutter = new();
private readonly System.Windows.Forms.Timer _hlTimer = new();
private bool _highlighting;
private const int WM_SETREDRAW = 0x000B;
private const int WM_USER = 0x0400;
private const int EM_GETFIRSTVISIBLELINE = WM_USER + 206;
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
public LuaEditor()
{
_gutter.Dock = DockStyle.Left;
_gutter.Width = 46;
_gutter.BackColor = Color.FromArgb(240, 243, 248);
_gutter.Paint += GutterPaint;
_rtb.Dock = DockStyle.Fill;
_rtb.BorderStyle = BorderStyle.None;
_rtb.Font = new Font("Consolas", 11F);
_rtb.BackColor = Color.FromArgb(252, 253, 255);
_rtb.ForeColor = Color.FromArgb(30, 34, 42);
_rtb.WordWrap = false;
_rtb.AcceptsTab = true;
_rtb.ScrollBars = RichTextBoxScrollBars.Both;
_rtb.DetectUrls = false;
_rtb.HideSelection = false;
_rtb.VScroll += (s, e) => _gutter.Invalidate();
_rtb.HScroll += (s, e) => _gutter.Invalidate();
_rtb.TextChanged += (s, e) =>
{
_hlTimer.Stop();
_hlTimer.Start();
ScriptChanged?.Invoke(this, EventArgs.Empty);
_gutter.Invalidate();
};
_rtb.SelectionChanged += (s, e) => CaretMoved?.Invoke(LineOf(_rtb.SelectionStart), ColumnOf(_rtb.SelectionStart));
_rtb.KeyDown += (s, e) =>
{
if (e.Control && e.KeyCode == Keys.A)
{
_rtb.SelectAll();
e.SuppressKeyPress = true;
}
// Tab 插入 4 空格而不是制表符
if (e.KeyCode == Keys.Tab && !e.Shift && !e.Control)
{
_rtb.SelectedText = " ";
e.SuppressKeyPress = true;
}
};
_hlTimer.Interval = 180;
_hlTimer.Tick += (s, e) => { _hlTimer.Stop(); Highlight(); };
Controls.Add(_rtb);
Controls.Add(_gutter);
}
/// <summary>脚本文本(不用基类 Text,避免与控件标题混淆)。</summary>
[System.ComponentModel.Browsable(false)]
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
public string Script
{
get => _rtb.Text;
set { _rtb.Text = value; Highlight(); }
}
public RichTextBox Inner => _rtb;
public event EventHandler? ScriptChanged;
public event Action<int, int>? CaretMoved;
public void FocusEditor() => _rtb.Focus();
public int CurrentLine => LineOf(_rtb.SelectionStart);
public void GotoLine(int line)
{
var lines = _rtb.Lines;
if (line < 0 || line >= lines.Length) return;
int idx = 0;
for (int i = 0; i < line; i++) idx += lines[i].Length + 1;
_rtb.SelectionStart = idx;
_rtb.SelectionLength = 0;
_rtb.ScrollToCaret();
_rtb.Focus();
}
private int LineOf(int charIndex)
{
int line = _rtb.GetLineFromCharIndex(charIndex);
return line + 1;
}
private int ColumnOf(int charIndex)
{
int first = _rtb.GetFirstCharIndexFromLine(_rtb.GetLineFromCharIndex(charIndex));
return charIndex - first + 1;
}
// ===================== 行号 =====================
private void GutterPaint(object? sender, PaintEventArgs e)
{
var g = e.Graphics;
g.Clear(_gutter.BackColor);
using var font = new Font("Consolas", 9.5F);
using var brush = new SolidBrush(Color.FromArgb(130, 142, 162));
using var curBrush = new SolidBrush(Color.FromArgb(37, 99, 235));
int firstVisible = (int)SendMessage(_rtb.Handle, EM_GETFIRSTVISIBLELINE, IntPtr.Zero, IntPtr.Zero);
int lineCount = _rtb.Lines.Length;
int lineHeight = _rtb.Font.Height;
int curLine = _rtb.GetLineFromCharIndex(_rtb.SelectionStart);
for (int i = 0; i < 200; i++)
{
int lineIdx = firstVisible + i;
if (lineIdx >= lineCount) break;
int y = i * lineHeight + 2;
if (y > _gutter.Height) break;
string num = (lineIdx + 1).ToString();
var sz = g.MeasureString(num, font);
var b = lineIdx == curLine ? curBrush : brush;
g.DrawString(num, font, b, _gutter.Width - sz.Width - 8, y);
}
}
// ===================== 语法着色 =====================
private static readonly Regex RxComment = new(@"--\[\[.*?\]\]|--[^\n]*", RegexOptions.Compiled | RegexOptions.Singleline);
private static readonly Regex RxString = new("\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'", RegexOptions.Compiled);
private static readonly Regex RxNumber = new(@"\b0x[0-9a-fA-F]+\b|\b\d+(\.\d+)?\b", RegexOptions.Compiled);
private static readonly Regex RxKeyword = new(
@"\b(and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b",
RegexOptions.Compiled);
private static readonly Regex RxApi = new(
@"\b(delay|delayRandom|moveTo|moveR|leftClick|rightClick|middleClick|doubleClick|wheel|drag|keyPress|keyDown|keyUp|hotkey|typeText|sayText|findImage|findColor|waitImage|exists|screenshot|colorAt|clipGet|clipSet|run|activate|log|stop|random|now|sleep|mouse|key|screen|win|sys|clip|browser)\b",
RegexOptions.Compiled);
private static readonly Color CKeyword = Color.FromArgb(175, 45, 130);
private static readonly Color CString = Color.FromArgb(20, 130, 80);
private static readonly Color CComment = Color.FromArgb(140, 150, 165);
private static readonly Color CNumber = Color.FromArgb(198, 106, 22);
private static readonly Color CApi = Color.FromArgb(30, 90, 200);
private static readonly Color CPlain = Color.FromArgb(30, 34, 42);
public void Highlight()
{
if (_highlighting || _rtb.TextLength == 0) return;
_highlighting = true;
try
{
string text = _rtb.Text;
int selStart = _rtb.SelectionStart;
int selLen = _rtb.SelectionLength;
SendMessage(_rtb.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero);
_rtb.SelectAll();
_rtb.SelectionColor = CPlain;
void Paint(Regex rx, Color color)
{
foreach (Match m in rx.Matches(text))
{
if (m.Length == 0) continue;
_rtb.Select(m.Index, m.Length);
_rtb.SelectionColor = color;
}
}
Paint(RxNumber, CNumber);
Paint(RxKeyword, CKeyword);
Paint(RxApi, CApi);
Paint(RxString, CString);
Paint(RxComment, CComment);
_rtb.Select(selStart, selLen);
_rtb.SelectionColor = CPlain;
SendMessage(_rtb.Handle, WM_SETREDRAW, (IntPtr)1, IntPtr.Zero);
_rtb.Invalidate();
}
catch { }
finally { _highlighting = false; }
}
}
}
+1995
View File
File diff suppressed because it is too large. Load diff
+133
View File
@@ -0,0 +1,133 @@
using anjianjingling.Native;
namespace anjianjingling.Forms
{
/// <summary>
/// 全屏框选截图:鼠标拖拽框选区域,松开后截图保存到 images 目录。
/// ESC 取消。
/// </summary>
public sealed class OverlayForm : Form
{
private Point _start, _end;
private bool _dragging;
private Rectangle _selected;
public string? SavedPath { get; private set; }
public event Action<string>? OnSaved;
/// <summary>是否把框选结果保存为图片(录屏取区域时设为 false)。</summary>
[System.ComponentModel.Browsable(false)]
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
public bool SaveToFile { get; set; } = true;
/// <summary>最近一次框选的屏幕矩形。</summary>
[System.ComponentModel.Browsable(false)]
[System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)]
public Rectangle SelectedRect { get; private set; }
public OverlayForm()
{
FormBorderStyle = FormBorderStyle.None;
StartPosition = FormStartPosition.Manual;
Bounds = SystemInformation.VirtualScreen;
TopMost = true;
ShowInTaskbar = false;
DoubleBuffered = true;
KeyPreview = true;
BackColor = Color.Black;
Opacity = 0.25;
Cursor = Cursors.Cross;
MouseDown += (s, e) =>
{
if (e.Button == MouseButtons.Left)
{
_dragging = true;
_start = _end = PointToScreen(e.Location);
}
};
MouseMove += (s, e) =>
{
if (_dragging)
{
_end = PointToScreen(e.Location);
var r = RectangleFromPoints(_start, _end);
_selected = r;
Invalidate();
}
};
MouseUp += (s, e) =>
{
if (_dragging && e.Button == MouseButtons.Left)
{
_dragging = false;
var r = RectangleFromPoints(_start, _end);
if (r.Width > 4 && r.Height > 4)
{
SelectedRect = r;
if (SaveToFile)
{
string dir = Path.Combine(AppContext.BaseDirectory, "images");
Directory.CreateDirectory(dir);
string file = Path.Combine(dir,
$"clip_{DateTime.Now:yyyyMMdd_HHmmss}.png");
using (var bmp = new Bitmap(r.Width, r.Height))
using (var g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(r.Left, r.Top, 0, 0, r.Size);
bmp.Save(file, System.Drawing.Imaging.ImageFormat.Png);
}
SavedPath = file;
OnSaved?.Invoke(file);
}
DialogResult = DialogResult.OK;
Close();
return;
}
Invalidate();
}
};
KeyDown += (s, e) =>
{
if (e.KeyCode == Keys.Escape)
{
DialogResult = DialogResult.Cancel;
Close();
}
};
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (_selected.Width > 0)
{
// 用屏幕坐标转客户区绘制
var r = ScreenRectToClient(_selected);
using (var pen = new Pen(Color.Lime, 1))
{
e.Graphics.DrawRectangle(pen, r);
string info = $"{_selected.Width} x {_selected.Height} ({_selected.Left},{_selected.Top})";
using var font = new Font("Consolas", 9F);
var sz = e.Graphics.MeasureString(info, font);
float ty = r.Top - sz.Height - 2;
if (ty < 0) ty = r.Bottom + 2;
using var bg = new SolidBrush(Color.Lime);
e.Graphics.FillRectangle(bg, r.Left, ty, sz.Width, sz.Height);
e.Graphics.DrawString(info, font, Brushes.Black, r.Left, ty);
}
}
}
private static Rectangle RectangleFromPoints(Point a, Point b)
=> Rectangle.FromLTRB(
Math.Min(a.X, b.X), Math.Min(a.Y, b.Y),
Math.Max(a.X, b.X), Math.Max(a.Y, b.Y));
private Rectangle ScreenRectToClient(Rectangle screen)
{
var a = PointToClient(new Point(screen.Left, screen.Top));
var b = PointToClient(new Point(screen.Right, screen.Bottom));
return new Rectangle(a.X, a.Y, b.X - a.X, b.Y - a.Y);
}
}
}
+208
View File
@@ -0,0 +1,208 @@
using System.Drawing.Drawing2D;
using anjianjingling.Common;
namespace anjianjingling.Forms
{
/// <summary>坐标取色器:置顶小窗,实时显示鼠标坐标与屏幕颜色,可复制坐标/颜色或写入日志。</summary>
public sealed class PickerForm : Form
{
private const int MagZoom = 7;
private const int SampleSize = 15;
private readonly LogBus _log;
private readonly System.Windows.Forms.Timer _timer = new() { Interval = 60 };
private readonly Label _lblPos = new();
private readonly Label _lblHex = new();
private readonly Label _lblRgb = new();
private readonly Panel _mag = new();
private readonly Panel _swatch = new();
private readonly Label _title = new();
private readonly Label _btnClose = new();
private Bitmap? _sample;
private Point _cursor;
private Color _color = Color.Black;
private Point _dragOffset;
private bool _dragging;
public PickerForm(LogBus log)
{
_log = log;
Text = "坐标取色器";
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
TopMost = true;
StartPosition = FormStartPosition.Manual;
ClientSize = new Size(288, 188);
BackColor = Color.FromArgb(30, 34, 42);
Font = new Font("Microsoft YaHei UI", 9F);
KeyPreview = true;
var start = Cursor.Position;
Location = new Point(
Math.Max(0, Math.Min(Screen.PrimaryScreen!.WorkingArea.Right - Width - 24, start.X + 40)),
Math.Max(0, Math.Min(Screen.PrimaryScreen!.WorkingArea.Bottom - Height - 24, start.Y + 40)));
// 顶栏
_title.Text = " 坐标取色器";
_title.Dock = DockStyle.None;
_title.SetBounds(0, 0, 240, 26);
_title.TextAlign = ContentAlignment.MiddleLeft;
_title.ForeColor = Color.FromArgb(220, 228, 240);
_title.Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
_title.BackColor = Color.FromArgb(42, 48, 60);
_btnClose.Text = "✕";
_btnClose.SetBounds(ClientSize.Width - 26, 0, 26, 26);
_btnClose.TextAlign = ContentAlignment.MiddleCenter;
_btnClose.ForeColor = Color.FromArgb(200, 210, 225);
_btnClose.BackColor = Color.FromArgb(42, 48, 60);
_btnClose.Cursor = Cursors.Hand;
_btnClose.Click += (s, e) => Close();
_btnClose.MouseEnter += (s, e) => _btnClose.BackColor = Color.FromArgb(190, 70, 60);
_btnClose.MouseLeave += (s, e) => _btnClose.BackColor = Color.FromArgb(42, 48, 60);
// 放大镜
_mag.SetBounds(10, 34, SampleSize * MagZoom, SampleSize * MagZoom);
_mag.BackColor = Color.FromArgb(20, 24, 30);
_mag.Paint += MagPaint;
// 色块
_swatch.SetBounds(124, 34, 62, 46);
_swatch.Paint += (s, e) =>
{
e.Graphics.Clear(_color);
using var pen = new Pen(Color.FromArgb(90, 98, 112));
e.Graphics.DrawRectangle(pen, 0, 0, _swatch.Width - 1, _swatch.Height - 1);
};
// 文字
_lblPos.SetBounds(124, 86, 160, 18);
_lblHex.SetBounds(124, 106, 160, 18);
_lblRgb.SetBounds(124, 126, 160, 18);
foreach (var l in new[] { _lblPos, _lblHex, _lblRgb })
{
l.ForeColor = Color.FromArgb(214, 222, 235);
l.Font = new Font("Consolas", 9.5F);
}
_lblPos.Text = "坐标 -";
_lblHex.Text = "颜色 -";
_lblRgb.Text = "RGB -";
// 按钮
var bar = new FlowLayoutPanel
{
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false
};
bar.SetBounds(10, 150, 268, 30);
bar.Controls.Add(MkBtn("复制坐标", () =>
Copy(_cursor.X + ", " + _cursor.Y)));
bar.Controls.Add(MkBtn("复制颜色", () =>
Copy($"{_color.R:X2}{_color.G:X2}{_color.B:X2}")));
bar.Controls.Add(MkBtn("写入日志", () =>
_log.Write($"取色:坐标 ({_cursor.X}, {_cursor.Y}) 颜色 #{_color.R:X2}{_color.G:X2}{_color.B:X2}")));
Controls.AddRange(new Control[] { _mag, _swatch, _lblPos, _lblHex, _lblRgb, bar, _title, _btnClose });
KeyDown += (s, e) => { if (e.KeyCode == Keys.Escape) Close(); };
MouseDown += DragStart;
_title.MouseDown += DragStart;
MouseMove += DragMove;
_title.MouseMove += DragMove;
MouseUp += (s, e) => _dragging = false;
_timer.Tick += (s, e) => Tick();
_timer.Start();
FormClosed += (s, e) => { _timer.Stop(); _sample?.Dispose(); };
}
private Button MkBtn(string text, Action onClick)
{
var b = new Button
{
Text = text, Width = 84, Height = 26, Margin = new Padding(0, 2, 4, 0),
FlatStyle = FlatStyle.Flat, BackColor = Color.FromArgb(52, 60, 74),
ForeColor = Color.FromArgb(222, 230, 242), Cursor = Cursors.Hand,
Font = new Font("Microsoft YaHei UI", 8.5F)
};
b.FlatAppearance.BorderColor = Color.FromArgb(70, 80, 96);
b.Click += (s, e) => onClick();
return b;
}
private void Copy(string text)
{
try { Clipboard.SetText(text); _log.Write("已复制:" + text); } catch { }
}
private void DragStart(object? s, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
_dragging = true;
_dragOffset = e.Location;
if (s != _title && s != this) _dragOffset = new Point(e.X + (s as Control)!.Left, e.Y + (s as Control)!.Top + 26);
}
private void DragMove(object? s, MouseEventArgs e)
{
if (!_dragging) return;
var screen = s == this ? PointToScreen(e.Location) : (s as Control)!.PointToScreen(e.Location);
Location = new Point(screen.X - _dragOffset.X, screen.Y - _dragOffset.Y);
}
private void Tick()
{
if (IsDisposed) return;
var p = Cursor.Position;
if (p == _cursor && _sample != null) return;
_cursor = p;
try
{
int half = SampleSize / 2;
_sample?.Dispose();
_sample = new Bitmap(SampleSize, SampleSize);
using (var g = Graphics.FromImage(_sample))
g.CopyFromScreen(p.X - half, p.Y - half, 0, 0, new Size(SampleSize, SampleSize));
_color = _sample.GetPixel(half, half);
}
catch
{
return;
}
_lblPos.Text = $"坐标 {_cursor.X}, {_cursor.Y}";
_lblHex.Text = $"颜色 #{_color.R:X2}{_color.G:X2}{_color.B:X2}";
_lblRgb.Text = $"RGB {_color.R}, {_color.G}, {_color.B}";
_mag.Invalidate();
_swatch.Invalidate();
}
private void MagPaint(object? s, PaintEventArgs e)
{
e.Graphics.Clear(Color.FromArgb(20, 24, 30));
if (_sample == null) return;
e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
e.Graphics.PixelOffsetMode = PixelOffsetMode.Half;
e.Graphics.DrawImage(_sample, new Rectangle(0, 0, _mag.Width, _mag.Height));
// 准星
int c = _mag.Width / 2;
using var pen = new Pen(Color.FromArgb(220, 255, 80, 80)) { Width = 1 };
e.Graphics.DrawLine(pen, c, 0, c, _mag.Height);
e.Graphics.DrawLine(pen, 0, c, _mag.Width, c);
using var inner = new Pen(Color.White) { Width = 1 };
e.Graphics.DrawRectangle(inner, c - MagZoom / 2, c - MagZoom / 2, MagZoom, MagZoom);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using var pen = new Pen(Color.FromArgb(64, 72, 86));
e.Graphics.DrawRectangle(pen, 0, 0, ClientSize.Width - 1, ClientSize.Height - 1);
}
}
}