824 lines
38 KiB
C#
824 lines
38 KiB
C#
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);
|
||
}
|
||
}
|