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); } }