first commit
This commit is contained in:
commit
761b95080f
404 files changed
+312608
No files matched your search
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user