Files
Ananta-ps/tools/launcher/Gfx.cs
T

824 lines
38 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
// 界面零件:配色、圆角、矢量小图标、圆角图、状态灯、卡片、几类按钮、开关。
// 配色与 GM 面板(127.0.0.1:17888)同源 —— 见 AdminWebServer.cs 的 :root 变量。
// 风格:纯色底 + 细描边,不用渐变纹路、不用光晕。
// 全部 GDI+ 自绘;除 app.ico 外不引入任何图片资源。
//
// ⛑ 透明注意事项:WinForms 里「子控件靠父级补画背景」这套在有自绘的父级上并不可靠,
// 所以子控件一律不透明 + 用 ambient 继承父级 BackColor。别再改回 Transparent。
internal static class Col
{
private static Color C(int hex) => Color.FromArgb((hex >> 16) & 0xff, (hex >> 8) & 0xff, hex & 0xff);
public static readonly Color Bg = C(0x0d1117); // --bg
public static readonly Color BgDeep = C(0x090d12); // 顶栏
public static readonly Color Card = C(0x161d27); // --card-1
public static readonly Color Card2 = C(0x101721); // --card-2
public static readonly Color Hover = C(0x18212c); // 悬停底
public static readonly Color Line = C(0x222b36); // --line
public static readonly Color LineSof = C(0x1a212b); // --line-soft
public static readonly Color Text = C(0xe6edf3); // --txt
public static readonly Color Text2 = C(0x9aa7b4); // --txt-2
public static readonly Color Text3 = C(0x69747f); // --txt-3
public static readonly Color Acc = C(0x2f7dff); // --acc
public static readonly Color Acc2 = C(0x1a5fe0); // --acc-2
public static readonly Color Ok = C(0x3fb950); // --ok
public static readonly Color Warn = C(0xf0a03c); // --warn
public static readonly Color Err = C(0xf44336);
public static readonly Color Violet = C(0x7e57c2);
public static readonly Color Edge = C(0x2b3a4d); // 图标描边色
}
// Objects = 2026-09-30 新增:世界物件(可破坏 / 吸取 / 可交互 / 室内)那个「立体箱子」图标,
// 刻意与注入器的 Syringe 区分开 —— 注入器是「打针」,这三个是「往世界里摆东西」。
// 2026-09-30 新增 Shield(盾牌 + 勾)⇒ 「安装证书」卡片用。
// Bolt = 2026-10-01 新增:闪电 ⇒ 「安装快速启动补丁」卡片用(跳过启动检测前的等待)。
// Car / Users = 2026-10-02 新增:启动器「车流 / 人流」两张并排开关卡片用。
internal enum Ico { None, Server, Syringe, Gamepad, Gm, Folder, Terminal, Play, Chevron, Stop, Objects, Shield, Bolt, Car, Users }
internal static class Gfx
{
public static GraphicsPath Round(RectangleF r, float rad)
{
var p = new GraphicsPath();
float d = rad * 2f;
if (d <= 0.5f) { p.AddRectangle(r); return p; }
if (d > r.Width) d = r.Width;
if (d > r.Height) d = r.Height;
p.AddArc(r.X, r.Y, d, d, 180, 90);
p.AddArc(r.Right - d, r.Y, d, d, 270, 90);
p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
p.AddArc(r.X, r.Bottom - d, d, d, 90, 90);
p.CloseAllFigures();
return p;
}
public static Region Region(RectangleF r, float rad)
{
using var gp = Round(r, rad);
return new Region(gp);
}
/// 把整个客户区铺一层纯色。
/// ⛑ 必须画在最前面:圆角控件本质是方形窗口,圆角外面的四小块如果不主动铺色,
/// 就会留下 Windows 未初始化的黑像素(表现就是「圆角周围有黑角」)。
public static void FillBase(Graphics g, Size sz, Color c)
{
using var b = new SolidBrush(c);
g.FillRectangle(b, 0, 0, sz.Width, sz.Height);
}
/// 让控件本身也是圆角的:窗口按圆角裁掉,四角就不会有残留像素
public static void ApplyRegion(Control c, float radius)
{
if (c.Width <= 0 || c.Height <= 0) return;
try { c.Region = Region(new RectangleF(0, 0, c.Width, c.Height), radius); } catch { }
}
public static void Fill(Graphics g, RectangleF r, float rad, Color c)
{
using var gp = Round(r, rad);
using var b = new SolidBrush(c);
g.FillPath(b, gp);
}
public static void FillGrad(Graphics g, RectangleF r, float rad, Color c1, Color c2, float angle)
{
using var gp = Round(r, rad);
using var b = new LinearGradientBrush(
new RectangleF(r.X, r.Y, Math.Max(1f, r.Width), Math.Max(1f, r.Height)), c1, c2, angle);
g.FillPath(b, gp);
}
public static void Stroke(Graphics g, RectangleF r, float rad, Color c, float w)
{
using var gp = Round(r, rad);
using var p = new Pen(c, w);
g.DrawPath(p, gp);
}
/// 把图片按圆角裁进指定矩形
public static void Image(Graphics g, Image img, RectangleF r, float rad)
{
if (img == null) return;
var old = g.Save();
using (var gp = Round(r, rad)) g.SetClip(gp);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.DrawImage(img, r);
g.Restore(old);
}
/// 圆角矩形内的垂直渐变文字(GM 面板品牌那种效果)
public static void GradText(Graphics g, string s, Font f, RectangleF box, Color top, Color bottom,
StringAlignment align, StringAlignment line)
{
using var br = new LinearGradientBrush(
new RectangleF(box.X, box.Y, Math.Max(1f, box.Width), Math.Max(1f, box.Height)), top, bottom, 90f);
using var sf = new StringFormat { Alignment = align, LineAlignment = line, FormatFlags = StringFormatFlags.NoWrap };
g.DrawString(s, f, br, box, sf);
}
public static void Icon(Graphics g, Ico kind, RectangleF r, Color c)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
float x = r.X, y = r.Y, w = r.Width, h = r.Height;
float sw = Math.Max(1.5f, w * 0.085f);
using var br = new SolidBrush(c);
switch (kind)
{
case Ico.Server:
{
float rh = h * 0.25f, gap = h * 0.115f;
for (int i = 0; i < 3; i++)
{
var rr = new RectangleF(x + w * 0.07f, y + i * (rh + gap), w * 0.86f, rh);
Fill(g, rr, rh * 0.36f, c);
using var d = new SolidBrush(Col.Bg);
g.FillEllipse(d, rr.Right - rr.Height * 0.72f, rr.Y + rr.Height * 0.34f, rh * 0.30f, rh * 0.30f);
g.FillEllipse(d, rr.Right - rr.Height * 0.44f, rr.Y + rr.Height * 0.34f, rh * 0.30f, rh * 0.30f);
using (var l = new Pen(Color.FromArgb(150, Col.Bg), Math.Max(1f, rh * 0.18f)))
g.DrawLine(l, rr.X + rr.Width * 0.10f, rr.Y + rr.Height * 0.5f, rr.X + rr.Width * 0.44f, rr.Y + rr.Height * 0.5f);
}
break;
}
case Ico.Syringe:
{
var st = g.Save();
g.TranslateTransform(x + w * 0.5f, y + h * 0.5f);
g.RotateTransform(-42f);
Fill(g, new RectangleF(-w * 0.34f, -h * 0.18f, w * 0.60f, h * 0.36f), h * 0.11f, c);
Fill(g, new RectangleF(-w * 0.46f, -h * 0.25f, w * 0.10f, h * 0.50f), h * 0.05f, c);
Fill(g, new RectangleF(-w * 0.38f, -h * 0.07f, w * 0.06f, h * 0.14f), 1f, c);
Fill(g, new RectangleF(-w * 0.51f, -h * 0.09f, w * 0.06f, h * 0.18f), h * 0.03f, c);
using (var np = new Pen(c, Math.Max(1.4f, w * 0.055f)) { StartCap = LineCap.Round, EndCap = LineCap.Round })
g.DrawLine(np, w * 0.26f, 0f, w * 0.48f, 0f);
using (var tp = new Pen(Color.FromArgb(200, Col.Bg), Math.Max(1.2f, w * 0.05f)))
for (int i = 0; i < 3; i++)
{
float tx = -w * 0.28f + i * w * 0.11f;
g.DrawLine(tp, tx, -h * 0.18f, tx, -h * 0.06f);
}
g.Restore(st);
break;
}
case Ico.Gamepad:
{
Fill(g, new RectangleF(x + w * 0.03f, y + h * 0.26f, w * 0.94f, h * 0.52f), h * 0.26f, c);
g.FillEllipse(br, x + w * 0.02f, y + h * 0.52f, w * 0.26f, h * 0.36f);
g.FillEllipse(br, x + w * 0.72f, y + h * 0.52f, w * 0.26f, h * 0.36f);
using var d = new SolidBrush(Col.Card);
g.FillRectangle(d, x + w * 0.20f, y + h * 0.48f, w * 0.18f, h * 0.075f);
g.FillRectangle(d, x + w * 0.252f, y + h * 0.42f, w * 0.075f, h * 0.19f);
g.FillEllipse(d, x + w * 0.60f, y + h * 0.46f, h * 0.12f, h * 0.12f);
g.FillEllipse(d, x + w * 0.72f, y + h * 0.38f, h * 0.12f, h * 0.12f);
break;
}
case Ico.Gm:
{
var m = new RectangleF(x + w * 0.03f, y + h * 0.08f, w * 0.94f, h * 0.66f);
Stroke(g, m, h * 0.12f, c, Math.Max(1.7f, sw * 0.9f));
Fill(g, new RectangleF(x + w * 0.42f, y + h * 0.74f, w * 0.16f, h * 0.10f), 1f, c);
Fill(g, new RectangleF(x + w * 0.24f, y + h * 0.84f, w * 0.52f, h * 0.07f), h * 0.03f, c);
using (var f = new Font("Segoe UI", Math.Max(6f, h * 0.34f), FontStyle.Bold, GraphicsUnit.Pixel))
using (var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center })
g.DrawString("GM", f, br, m, sf);
break;
}
case Ico.Folder:
{
Fill(g, new RectangleF(x + w * 0.03f, y + h * 0.16f, w * 0.44f, h * 0.16f), h * 0.06f, c);
Fill(g, new RectangleF(x + w * 0.03f, y + h * 0.26f, w * 0.94f, h * 0.56f), h * 0.14f, c);
break;
}
case Ico.Terminal:
{
var t = new RectangleF(x + w * 0.03f, y + h * 0.14f, w * 0.94f, h * 0.72f);
Stroke(g, t, h * 0.14f, c, Math.Max(1.7f, sw * 0.9f));
using (var p2 = new Pen(c, Math.Max(1.7f, sw * 0.8f)) { StartCap = LineCap.Round, EndCap = LineCap.Round })
{
g.DrawLine(p2, x + w * 0.22f, y + h * 0.40f, x + w * 0.34f, y + h * 0.50f);
g.DrawLine(p2, x + w * 0.34f, y + h * 0.50f, x + w * 0.22f, y + h * 0.60f);
g.DrawLine(p2, x + w * 0.44f, y + h * 0.62f, x + w * 0.72f, y + h * 0.62f);
}
break;
}
case Ico.Objects:
{
// 等距立方体(木箱):顶面菱形 + 左 / 右两个侧面;三条内部棱线。
// 顶亮、左中、右暗 ⇒ 一眼是「立体物件」,不是平面方框。
float cx = x + w * 0.5f;
float hw = w * 0.34f, hh = h * 0.16f, sh = h * 0.30f;
float top = y + h * 0.16f;
var T = new PointF(cx, top);
var R = new PointF(cx + hw, top + hh);
var B = new PointF(cx, top + hh * 2f);
var L = new PointF(cx - hw, top + hh);
var B2 = new PointF(cx, B.Y + sh);
var R2 = new PointF(R.X, R.Y + sh);
var L2 = new PointF(L.X, L.Y + sh);
using (var b1 = new SolidBrush(Color.FromArgb(230, c))) g.FillPolygon(b1, new[] { T, R, B, L });
using (var b2 = new SolidBrush(Color.FromArgb(150, c))) g.FillPolygon(b2, new[] { L, B, B2, L2 });
using (var b3 = new SolidBrush(Color.FromArgb(85, c))) g.FillPolygon(b3, new[] { B, R, R2, B2 });
using (var p2 = new Pen(c, Math.Max(1.6f, sw * 0.72f))
{ LineJoin = LineJoin.Round, StartCap = LineCap.Round, EndCap = LineCap.Round })
{
g.DrawPolygon(p2, new[] { T, R, R2, B2, L2, L }); // 外轮廓
g.DrawLine(p2, L, B); // 顶 / 左 的公共棱
g.DrawLine(p2, B, R); // 顶 / 右 的公共棱
g.DrawLine(p2, B, B2); // 左 / 右 的公共棱(正面竖线)
}
break;
}
case Ico.Shield:
{
// 盾牌(证书 / 信任):上宽下尖的盾形实心 + 底色勾。
using (var gp = new GraphicsPath())
{
gp.AddLine(x + w * 0.50f, y + h * 0.04f, x + w * 0.94f, y + h * 0.24f);
gp.AddLine(x + w * 0.94f, y + h * 0.24f, x + w * 0.50f, y + h * 0.98f);
gp.AddLine(x + w * 0.50f, y + h * 0.98f, x + w * 0.06f, y + h * 0.24f);
gp.CloseFigure();
g.FillPath(br, gp);
}
using (var p3 = new Pen(Col.Bg, Math.Max(1.8f, sw * 0.95f))
{ StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round })
{
g.DrawLine(p3, x + w * 0.30f, y + h * 0.50f, x + w * 0.44f, y + h * 0.64f);
g.DrawLine(p3, x + w * 0.44f, y + h * 0.64f, x + w * 0.72f, y + h * 0.34f);
}
break;
}
case Ico.Bolt:
{
// 闪电(快 / 提速):上宽下尖的折线多边形实心。
using var gp = new GraphicsPath();
gp.AddPolygon(new[]
{
new PointF(x + w * 0.60f, y + h * 0.02f),
new PointF(x + w * 0.28f, y + h * 0.56f),
new PointF(x + w * 0.48f, y + h * 0.56f),
new PointF(x + w * 0.38f, y + h * 0.98f),
new PointF(x + w * 0.74f, y + h * 0.40f),
new PointF(x + w * 0.52f, y + h * 0.40f)
});
g.FillPath(br, gp);
break;
}
case Ico.Car:
{
// 车(车流):车顶梯形 + 车身圆角长条 + 两个轮子。
using (var gp = new GraphicsPath())
{
gp.AddPolygon(new[]
{
new PointF(x + w * 0.22f, y + h * 0.54f),
new PointF(x + w * 0.34f, y + h * 0.24f),
new PointF(x + w * 0.68f, y + h * 0.24f),
new PointF(x + w * 0.80f, y + h * 0.54f)
});
g.FillPath(br, gp);
}
Fill(g, new RectangleF(x + w * 0.04f, y + h * 0.50f, w * 0.92f, h * 0.30f), h * 0.09f, c);
float wr = Math.Max(2f, h * 0.12f);
using (var wb = new SolidBrush(c))
{
g.FillEllipse(wb, x + w * 0.24f - wr, y + h * 0.82f - wr, wr * 2f, wr * 2f);
g.FillEllipse(wb, x + w * 0.76f - wr, y + h * 0.82f - wr, wr * 2f, wr * 2f);
}
break;
}
case Ico.Users:
{
// 人群(人流):后排一个小的 + 前排一个大的(头 = 圆,身 = 圆角块)。
float r1 = Math.Max(2f, w * 0.13f), r2 = Math.Max(2f, w * 0.16f);
using (var b2 = new SolidBrush(c))
{
g.FillEllipse(b2, x + w * 0.60f - r1, y + h * 0.12f, r1 * 2f, r1 * 2f);
Fill(g, new RectangleF(x + w * 0.46f, y + h * 0.38f, w * 0.40f, h * 0.46f), r1 * 1.5f, c);
g.FillEllipse(b2, x + w * 0.32f - r2, y + h * 0.22f, r2 * 2f, r2 * 2f);
Fill(g, new RectangleF(x + w * 0.10f, y + h * 0.50f, w * 0.44f, h * 0.44f), r2 * 1.5f, c);
}
break;
}
case Ico.Play:
{
using var gp = new GraphicsPath();
gp.AddPolygon(new[]
{
new PointF(x + w * 0.30f, y + h * 0.18f),
new PointF(x + w * 0.30f, y + h * 0.82f),
new PointF(x + w * 0.82f, y + h * 0.50f)
});
g.FillPath(br, gp);
break;
}
case Ico.Stop:
{
float side = Math.Min(w, h) * 0.56f;
Fill(g, new RectangleF(x + (w - side) / 2f, y + (h - side) / 2f, side, side), side * 0.22f, c);
break;
}
case Ico.Chevron:
{
using var p2 = new Pen(c, Math.Max(2f, w * 0.12f)) { StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round };
g.DrawLines(p2, new[]
{
new PointF(x + w * 0.34f, y + h * 0.24f),
new PointF(x + w * 0.68f, y + h * 0.50f),
new PointF(x + w * 0.34f, y + h * 0.76f)
});
break;
}
}
}
}
/// 只画一个矢量小图标的方块
internal sealed class IconBox : Control
{
public Ico Kind = Ico.None;
public Color Tint = Col.Text;
public Color Surround = Col.Card;
public IconBox(Ico kind, Color tint)
{
Kind = kind; Tint = tint;
SetStyle(ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
TabStop = false;
}
protected override void OnPaint(PaintEventArgs e)
{
Gfx.FillBase(e.Graphics, ClientSize, Surround);
Gfx.Icon(e.Graphics, Kind, new RectangleF(0, 0, Width, Height), Tint);
}
}
/// 圆角图片框(标题栏左上角那个 app 图标)
internal sealed class RoundPic : Control
{
public Image Img;
public float Radius = 10f;
public Color Line = Col.Edge;
public Color Ring = Color.FromArgb(26, 0x2f, 0x7d, 0xff);
public Color Surround = Col.BgDeep;
public RoundPic(Image img)
{
Img = img;
SetStyle(ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
TabStop = false;
}
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); Gfx.ApplyRegion(this, Radius); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
Gfx.FillBase(g, ClientSize, Surround);
var r = new RectangleF(3.5f, 3.5f, Width - 7f, Height - 7f);
Gfx.Image(g, Img, r, Radius);
Gfx.Stroke(g, r, Radius, Line, 1.5f); // 圆角外边框
Gfx.Stroke(g, RectangleF.Inflate(r, 3f, 3f), Radius + 3f, Ring, 1.2f);
}
}
/// 状态灯:实心圆点,Busy 时跟着外部节拍闪
internal sealed class StatusDot : Control
{
public Color Tint = Col.Text3;
public bool Busy = false;
public bool Beat = true;
public Color Surround = Col.Card;
public StatusDot()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
TabStop = false;
}
public void Set(Color c, bool busy) { Tint = c; Busy = busy; Invalidate(); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
Gfx.FillBase(g, ClientSize, Surround);
float a = (Busy && !Beat) ? 0.35f : 1f;
var r = new RectangleF(0, 0, Width, Height);
using (var gb = new SolidBrush(Color.FromArgb((int)(44 * a), Tint.R, Tint.G, Tint.B)))
g.FillEllipse(gb, r.X - 2.5f, r.Y - 2.5f, r.Width + 5f, r.Height + 5f);
using var b = new SolidBrush(Color.FromArgb((int)(255 * a), Tint.R, Tint.G, Tint.B));
g.FillEllipse(b, r);
}
}
/// 圆角卡片(纯色底 + 细描边,可选分隔线)
// ⛑ 2026-10-01 去掉 sealed:ActionTile 要从它派生(并排可点卡片,复用圆角/底色那一套绘制)。
internal class Card : Panel
{
public float Radius = 12f;
public float DividerY = -1f;
public float DividerX1 = 26f;
public float DividerX2 = -1f;
public Color Surround = Col.Bg;
public Card()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
BackColor = Col.Card;
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
Gfx.ApplyRegion(this, Radius);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
Gfx.FillBase(g, ClientSize, Surround);
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
Gfx.Fill(g, r, Radius, BackColor);
Gfx.Stroke(g, r, Radius, Col.Line, 1f);
if (DividerY > 0)
{
using var p = new Pen(Col.Line, 1f);
float x2 = DividerX2 > 0 ? DividerX2 : Width - DividerX1;
g.DrawLine(p, DividerX1, DividerY, x2, DividerY);
}
}
}
/// 整块可点的行式卡片(左图标 + 标题 + 副标题 + 右雪佛龙)。
/// ⛑ 2026-10-01 新增:给「安装证书」/「安装快速启动补丁」并排两张用 —— 用户要求**点整张卡片就执行**,
/// 右边不要再放「安装」小按钮。所以这里**不塞任何子控件、全部自绘**:
/// 点到 Label / IconBox 上时 Panel.Click 是不会触发的,只有自绘才能整块响应。
/// ⛑ 两个字号是自绘字段,Control.Scale() 管不到 ⇒ 由 MainForm.ScaleFonts 单独缩(同 FlatBtn.TextPx)。
internal sealed class ActionTile : Card
{
public Ico Kind = Ico.None;
public Color Tint = Col.Acc;
public string Title = "";
public string Sub = "";
// ⛑ 2026-10-02:标题统一 20px 粗体,与状态卡里的「服务器状态 / 注入器状态」同号。
public float TitlePx = 20f;
public float SubPx = 15f;
private bool _hover, _down;
public ActionTile()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
Cursor = Cursors.Hand;
}
protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }
protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _hover = true; Invalidate(); }
protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _hover = false; _down = false; Invalidate(); }
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); _down = true; Invalidate(); }
protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _down = false; Invalidate(); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
Gfx.FillBase(g, ClientSize, Surround);
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
bool on = Enabled;
Gfx.Fill(g, r, Radius, on && (_hover || _down) ? Col.Hover : Col.Card);
Gfx.Stroke(g, r, Radius, on && (_hover || _down) ? Col.Acc : Col.Line, 1f);
float d = Math.Min(Height * 0.42f, 40f);
float ix = 30f;
Gfx.Icon(g, Kind, new RectangleF(ix, Height / 2f - d / 2f, d, d),
on ? Tint : Color.FromArgb(110, Col.Text3));
float tx = ix + d + 18f;
float avail = Math.Max(20f, Width - tx - 40f);
using var sf = new StringFormat { Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap };
using (var fb = new SolidBrush(on ? Col.Text : Col.Text3))
using (var ft = new Font("Microsoft YaHei UI", TitlePx, FontStyle.Bold, GraphicsUnit.Pixel))
g.DrawString(Title, ft, fb, new RectangleF(tx, Height * 0.19f, avail, Height * 0.34f), sf);
if (!string.IsNullOrEmpty(Sub))
{
using var fb2 = new SolidBrush(on ? Col.Text3 : Color.FromArgb(120, Col.Text3));
using var fs = new Font("Microsoft YaHei UI", SubPx, FontStyle.Regular, GraphicsUnit.Pixel);
g.DrawString(Sub, fs, fb2, new RectangleF(tx, Height * 0.55f, avail, Height * 0.34f), sf);
}
Gfx.Icon(g, Ico.Chevron, new RectangleF(Width - 38f, Height / 2f - 10f, 20f, 20f),
on ? Col.Text3 : Color.FromArgb(70, Col.Text3));
}
}
/// 主按键:圆形播放图标在上、文字在下(整块竖直居中)
internal sealed class HeroBtn : Button
{
public float Radius = 14f;
public Color Surround = Col.Card;
/// true = 「关闭服务端」态:灰蓝底 + 红色圆里一个白色停止方块,和蓝色「启动」态明显区分
public bool StopMode = false;
/// 图标底边与文字顶边之间的行距(px);「图标 + Gap + 文字」整体块仍竖直居中
public float Gap = 16f;
private bool _hover, _down;
public HeroBtn()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
BackColor = Col.Card;
Cursor = Cursors.Hand;
Font = new Font("Microsoft YaHei UI", 12f, FontStyle.Bold);
}
protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }
protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _hover = true; Invalidate(); }
protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _hover = false; _down = false; Invalidate(); }
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); _down = true; Invalidate(); }
protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _down = false; Invalidate(); }
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); Gfx.ApplyRegion(this, Radius); }
private static Color Mix(Color a, Color b, float t)
{
return Color.FromArgb((int)(a.R + (b.R - a.R) * t), (int)(a.G + (b.G - a.G) * t), (int)(a.B + (b.B - a.B) * t));
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
Gfx.FillBase(g, ClientSize, Surround);
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
bool on = Enabled;
if (StopMode)
{
// 关闭态:灰蓝(和蓝色的「启动」一眼能分清)
Color s1 = _down ? Mix(Col.LineSof, Col.BgDeep, 0.4f) : (_hover ? Mix(Col.LineSof, Col.Line, 0.6f) : Col.LineSof);
Color s2 = _down ? Col.BgDeep : (_hover ? Mix(Col.Line, Col.LineSof, 0.4f) : Col.Line);
Gfx.FillGrad(g, r, Radius, s1, s2, 90f);
Gfx.Stroke(g, r, Radius, _hover ? Col.Text2 : Col.Line, 1f);
}
else if (on)
{
Color c1 = _down ? Mix(Col.Acc, Col.Acc2, 0.9f) : (_hover ? Mix(Col.Acc, Color.White, 0.10f) : Col.Acc);
Color c2 = _down ? Col.Acc2 : (_hover ? Mix(Col.Acc, Col.Acc2, 0.75f) : Col.Acc2);
Gfx.FillGrad(g, r, Radius, c1, c2, 90f);
}
else
{
Gfx.Fill(g, r, Radius, Col.Card2);
Gfx.Stroke(g, r, Radius, Col.Line, 1f);
}
// 图标:圆 + 三角(启动·蓝) / 停止方块(关闭·红);文字在下,两者之间留 Gap
float d = Math.Min(60f, Math.Min(Width * 0.34f, Height * 0.34f));
float cx = Width / 2f;
bool dim = !on && !StopMode;
// 文字:按宽度自动缩号,保证完整显示(先量出来,才好把「图标+Gap+文字」整体居中)
Color fg = (on || StopMode) ? Color.White : Col.Text3;
float avail = Width - 28f;
Font font = null;
try
{
for (float size = Math.Min(28f, Height * 0.20f); size >= 11f; size -= 1f)
{
font?.Dispose();
font = new Font("Microsoft YaHei UI", size, FontStyle.Bold, GraphicsUnit.Pixel);
if (g.MeasureString(Text, font).Width <= avail) break;
}
font ??= new Font("Microsoft YaHei UI", 14f, FontStyle.Bold, GraphicsUnit.Pixel);
var sz = g.MeasureString(Text, font);
// 竖直布局:图标 + Gap + 文字,整块居中(原来是 0.34 / 0.66 硬比例,行距固定且偏上)
float total = d + Gap + sz.Height;
float top = (Height - total) / 2f;
float cyIcon = top + d / 2f;
float textY = top + d + Gap;
using (var wb = new SolidBrush(dim ? Mix(Col.Card2, Col.Text3, 0.5f) : (StopMode ? Col.Err : Color.White)))
g.FillEllipse(wb, cx - d / 2f, cyIcon - d / 2f, d, d);
using (var tb = new SolidBrush(dim ? Col.Card2 : (StopMode ? Color.White : Col.Acc)))
{
if (StopMode)
{
float side = d * 0.42f;
Gfx.Fill(g, new RectangleF(cx - side / 2f, cyIcon - side / 2f, side, side), side * 0.2f, tb.Color);
}
else
{
float t = d * 0.42f;
using var tri = new GraphicsPath();
tri.AddPolygon(new[]
{
new PointF(cx - t * 0.40f, cyIcon - t * 0.58f),
new PointF(cx - t * 0.40f, cyIcon + t * 0.58f),
new PointF(cx + t * 0.60f, cyIcon)
});
g.FillPath(tb, tri);
}
}
using var tbTxt = new SolidBrush(fg);
g.DrawString(Text, font, tbTxt, cx - sz.Width / 2f, textY);
}
finally { font?.Dispose(); }
}
}
/// 卡片式方按键(左图标 + 文字 + 右雪佛龙)
internal sealed class TileBtn : Button
{
public Ico Kind = Ico.None;
public Color Tint = Col.Acc;
public float Radius = 12f;
public Color Surround = Col.Bg;
/// true = 「游戏已启动」态:绿色图标 + 亮字 + 绿描边。
/// ⛑ 这个态也是 Enabled=false(防重复启动),但视觉上必须和「服务端没起 → 灰掉」区分开。
public bool DoneMode = false;
private bool _hover, _down;
public TileBtn()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
BackColor = Col.Card;
Cursor = Cursors.Hand;
Font = new Font("Microsoft YaHei UI", 13.5f, FontStyle.Bold);
}
protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }
protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _hover = true; Invalidate(); }
protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _hover = false; _down = false; Invalidate(); }
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); _down = true; Invalidate(); }
protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _down = false; Invalidate(); }
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); Gfx.ApplyRegion(this, Radius); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
Gfx.FillBase(g, ClientSize, Surround);
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
bool on = Enabled;
Gfx.Fill(g, r, Radius, on && (_hover || _down) ? Col.Hover : Col.Card);
Gfx.Stroke(g, r, Radius, on && _hover ? Col.Acc : (DoneMode ? Color.FromArgb(110, Col.Ok) : Col.Line), 1f);
Color iconTint = DoneMode ? Col.Ok : (on ? Tint : Color.FromArgb(110, Col.Text3));
float d = Math.Min(Height * 0.42f, 42f);
float ix = 30f;
Gfx.Icon(g, Kind, new RectangleF(ix, Height / 2f - d / 2f, d, d), iconTint);
Color fg = DoneMode ? Col.Text : (on ? Col.Text : Col.Text3);
using (var b = new SolidBrush(fg))
// ⛑ 2026-10-02:19 → 20px,与「服务器状态 / 注入器状态」标题同号(用户要求统一)。
using (var f = new Font("Microsoft YaHei UI", 20f, FontStyle.Bold, GraphicsUnit.Pixel))
{
var sz = g.MeasureString(Text, f);
g.DrawString(Text, f, b, ix + d + 18f, Height / 2f - sz.Height / 2f);
}
Gfx.Icon(g, Ico.Chevron, new RectangleF(Width - 38f, Height / 2f - 10f, 20f, 20f),
on ? Col.Text3 : Color.FromArgb(70, Col.Text3));
}
}
/// 描边小按钮(次要操作:图标 + 文字)
internal sealed class FlatBtn : Button
{
public Ico Kind = Ico.None;
public float Radius = 8f;
public float TextPx = 15f;
public Color Surround = Col.Card;
private bool _hover, _down;
public FlatBtn()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
BackColor = Col.Card;
Cursor = Cursors.Hand;
Font = new Font("Microsoft YaHei UI", 10f, FontStyle.Bold);
}
protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }
protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _hover = true; Invalidate(); }
protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _hover = false; _down = false; Invalidate(); }
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); _down = true; Invalidate(); }
protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _down = false; Invalidate(); }
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); Gfx.ApplyRegion(this, Radius); }
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
Gfx.FillBase(g, ClientSize, Surround);
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
Gfx.Fill(g, r, Radius, (_hover || _down) ? Col.Hover : Col.Card2);
Gfx.Stroke(g, r, Radius, _hover ? Col.Acc : Col.Line, 1f);
float iw = Math.Max(15f, TextPx * 1.25f);
using var font = new Font("Microsoft YaHei UI", TextPx, FontStyle.Bold, GraphicsUnit.Pixel);
var sz = g.MeasureString(Text, font);
float total = sz.Width + (Kind != Ico.None ? iw + 7f : 0f);
float sx = (Width - total) / 2f;
float cy = Height / 2f;
if (Kind != Ico.None)
{
Gfx.Icon(g, Kind, new RectangleF(sx, cy - iw / 2f, iw, iw), Col.Acc);
sx += iw + 7f;
}
using var tb = new SolidBrush(Col.Text);
g.DrawString(Text, font, tb, sx, cy - sz.Height / 2f);
}
}
/// 滑动开关
internal sealed class Toggle : Control
{
public bool Checked = false;
public Color Surround = Col.Card;
private int _knob = 0;
private readonly Timer _anim = new Timer { Interval = 15 };
public event EventHandler CheckedChanged;
public Toggle()
{
// ⛑ 不用 Transparent:父级是自绘的,借不到背景就会露出黑块。
// 改成不透明 + 自己铺 Surround + 圆角 Region。
SetStyle(ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
BackColor = Col.Card;
Cursor = Cursors.Hand;
TabStop = false;
_anim.Tick += (s, e) =>
{
int target = Checked ? Width - Height : 0;
if (_knob == target) { _anim.Stop(); return; }
int d = target - _knob;
_knob += Math.Sign(d) * Math.Max(2, Math.Abs(d) / 3);
if (Math.Abs(target - _knob) < 2) _knob = target;
Invalidate();
};
}
public void SetChecked(bool v)
{
if (Checked == v) return;
Checked = v; _anim.Start(); Invalidate();
}
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); Gfx.ApplyRegion(this, Height / 2f); }
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
SetChecked(!Checked);
CheckedChanged?.Invoke(this, EventArgs.Empty);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
Gfx.FillBase(g, ClientSize, Surround);
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
Gfx.Fill(g, r, Height / 2f, Checked ? Col.Acc : Col.LineSof);
Gfx.Stroke(g, r, Height / 2f, Checked ? Col.Acc : Col.Line, 1f);
float d = Height - 8f;
g.FillEllipse(new SolidBrush(Checked ? Color.White : Col.Text3), 4f + _knob, 4f, d, d);
}
}