using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace TfsExcelImporter
{
///
/// 设计 token:颜色、字号、间距、圆角。界面代码只引用这里,不散落具体色值。
///
internal static class Theme
{
// --- 缩放 ---
/// 系统缩放系数(96dpi = 1)。必须在 BuildUi 之前按 DeviceDpi 赋值。
public static float Scale = 1f;
/// 设计稿(96dpi)像素 → 当前缩放下的像素。字体用磅值,Windows 会自动跟缩放,所以只有像素量需要换算。
public static int Px(int designPx)
{
return (int)Math.Round(designPx * Scale);
}
// --- 间距刻度(4/8/12/16) ---
public static int S4 { get { return Px(4); } }
public static int S8 { get { return Px(8); } }
public static int S12 { get { return Px(12); } }
public static int S16 { get { return Px(16); } }
public static int Radius { get { return Px(8); } }
public static int RadiusCtl { get { return Px(6); } }
public static int FieldH { get { return Px(32); } }
public static int BtnH { get { return Px(32); } }
// --- 颜色 ---
public static readonly Color Surface = Color.FromArgb(0xF4, 0xF6, 0xFA);
public static readonly Color Card = Color.White;
public static readonly Color Border = Color.FromArgb(0xDF, 0xE3, 0xEB);
public static readonly Color BorderStrong = Color.FromArgb(0xC7, 0xCE, 0xDB);
public static readonly Color Ink = Color.FromArgb(0x1F, 0x24, 0x30);
public static readonly Color Ink2 = Color.FromArgb(0x59, 0x62, 0x70);
public static readonly Color Ink3 = Color.FromArgb(0x8A, 0x93, 0xA3);
public static readonly Color Primary = Color.FromArgb(0x1E, 0x40, 0xAF);
public static readonly Color PrimaryHover = Color.FromArgb(0x1B, 0x3A, 0x9E);
public static readonly Color PrimaryDown = Color.FromArgb(0x17, 0x32, 0x88);
public static readonly Color PrimarySoft = Color.FromArgb(0xEA, 0xF0, 0xFB);
public static readonly Color PrimarySoftBorder = Color.FromArgb(0xCF, 0xDD, 0xF5);
public static readonly Color Accent = Color.FromArgb(0xB4, 0x53, 0x09);
public static readonly Color AccentSoft = Color.FromArgb(0xFD, 0xF3, 0xE3);
public static readonly Color AccentBorder = Color.FromArgb(0xF0, 0xD9, 0xB0);
public static readonly Color Ok = Color.FromArgb(0x15, 0x80, 0x3D);
public static readonly Color OkSoft = Color.FromArgb(0xE8, 0xF6, 0xED);
public static readonly Color OkBorder = Color.FromArgb(0xBF, 0xE3, 0xCC);
public static readonly Color Danger = Color.FromArgb(0xDC, 0x26, 0x26);
public static readonly Color DangerSoft = Color.FromArgb(0xFD, 0xEC, 0xEC);
public static readonly Color DangerBorder = Color.FromArgb(0xF6, 0xC7, 0xC7);
public static readonly Color Hover = Color.FromArgb(0xF2, 0xF5, 0xFA);
public static readonly Color Disabled = Color.FromArgb(0xEE, 0xF1, 0xF6);
// 次按钮的浅色调底:白底卡片上靠色相而非描边区分层级
public static readonly Color Tonal = Color.FromArgb(0xF5, 0xF7, 0xFB);
public static readonly Color TonalDown = Color.FromArgb(0xE4, 0xEA, 0xF5);
public static readonly Color TonalBorder = Color.FromArgb(0xDC, 0xE2, 0xEB);
public static readonly Color RowAlt = Color.FromArgb(0xFA, 0xFB, 0xFD);
public static readonly Color LogBg = Color.FromArgb(0xFB, 0xFC, 0xFE);
// --- 字体(CJK 优先,缺字体时逐级回落) ---
public static readonly Font Body = Make("Microsoft YaHei UI", 9.75f, false, false);
public static readonly Font BodyBold = Make("Microsoft YaHei UI", 9.75f, true, false);
public static readonly Font Caption = Make("Microsoft YaHei UI", 9f, false, false);
public static readonly Font CaptionBold = Make("Microsoft YaHei UI", 9f, true, false);
public static readonly Font Mono = Make("Consolas", 9.75f, false, true);
public static readonly Font MonoBold = Make("Consolas", 9.75f, true, true);
public static readonly Font CountNum = Make("Consolas", 11.25f, true, true);
private static Font Make(string family, float size, bool bold, bool monospace)
{
var style = bold ? FontStyle.Bold : FontStyle.Regular;
if (monospace)
{
try { return new Font(family, size, style, GraphicsUnit.Point); }
catch { return new Font(FontFamily.GenericMonospace, size, style, GraphicsUnit.Point); }
}
foreach (var name in new[] { family, "Microsoft YaHei UI", "Microsoft YaHei", "Segoe UI" })
{
try { return new Font(name, size, style, GraphicsUnit.Point); }
catch { }
}
return new Font(FontFamily.GenericSansSerif, size, style, GraphicsUnit.Point);
}
public static GraphicsPath RoundRect(RectangleF r, float radius)
{
var path = new GraphicsPath();
float d = radius * 2f;
if (d <= 0f || r.Width <= 0f || r.Height <= 0f)
{
path.AddRectangle(r);
path.CloseFigure();
return path;
}
d = Math.Min(d, Math.Min(r.Width, r.Height));
path.AddArc(r.X, r.Y, d, d, 180f, 90f);
path.AddArc(r.Right - d, r.Y, d, d, 270f, 90f);
path.AddArc(r.Right - d, r.Bottom - d, d, d, 0f, 90f);
path.AddArc(r.X, r.Bottom - d, d, d, 90f, 90f);
path.CloseFigure();
return path;
}
/// 细描边固定 1 设备像素(跟 Windows 自身控件一致),不随缩放变粗。
public static float Hair { get { return 1f; } }
/// 强调描边(焦点/校验失败)按缩放取整,150% 下为 2 像素。
public static float HairStrong { get { return Math.Max(1f, (float)Math.Round(Scale)); } }
/// 填充 + 描边一个圆角面板;borderWidth 为 0 时只填充。
/// Half 像素偏移 + 内嵌画笔让 1px 边线落在整像素上,避免高缩放下糊成双线。
/// 圆角面内切在 r 里,四角和最后 1 行列落在它外面,必须先用 backdrop 铺满整块客户区:
/// 自绘控件的双缓冲只清一次,真实屏幕上那些从未绘制的像素会残留成黑色描边。
public static void FillPanel(Graphics g, Rectangle r, Color fill, Color borderColor,
int radius, float borderWidth, Color backdrop)
{
if (r.Width < 2 || r.Height < 2) return;
var old = g.SmoothingMode;
var oldOffset = g.PixelOffsetMode;
if (backdrop.A > 0)
using (var bg = new SolidBrush(backdrop)) g.FillRectangle(bg, r);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.Half;
var box = new RectangleF(r.X, r.Y, r.Width - 1f, r.Height - 1f);
using (var path = RoundRect(box, radius))
{
using (var b = new SolidBrush(fill)) g.FillPath(b, path);
if (borderColor != Color.Transparent && borderWidth > 0f)
using (var pen = new Pen(borderColor, borderWidth))
{
pen.Alignment = PenAlignment.Inset;
g.DrawPath(pen, path);
}
}
g.PixelOffsetMode = oldOffset;
g.SmoothingMode = old;
}
/// 控件四周应当露出的底色:优先父容器面色,透明底(自绘宿主)退到卡片白。
public static Color BackdropOf(Control owner)
{
var parent = owner.Parent;
if (parent != null && parent.BackColor.A > 0) return parent.BackColor;
if (owner.BackColor.A > 0) return owner.BackColor;
return Card;
}
public static int TextWidth(string text, Font font) {
if (string.IsNullOrEmpty(text)) return 0;
return TextRenderer.MeasureText(text, font, Size.Empty,
TextFormatFlags.NoPadding | TextFormatFlags.SingleLine).Width;
}
public static Color ChipInk(ChipKind kind)
{
switch (kind)
{
case ChipKind.Ok: return Ok;
case ChipKind.Warn: return Accent;
case ChipKind.Danger: return Danger;
case ChipKind.Info: return Primary;
case ChipKind.Quiet: return Ink3;
default: return Ink2;
}
}
public static Color ChipFace(ChipKind kind)
{
switch (kind)
{
case ChipKind.Ok: return OkSoft;
case ChipKind.Warn: return AccentSoft;
case ChipKind.Danger: return DangerSoft;
case ChipKind.Info: return PrimarySoft;
case ChipKind.Quiet: return Card;
default: return Disabled;
}
}
public static Color ChipBorder(ChipKind kind)
{
switch (kind)
{
case ChipKind.Ok: return OkBorder;
case ChipKind.Warn: return AccentBorder;
case ChipKind.Danger: return DangerBorder;
case ChipKind.Info: return PrimarySoftBorder;
case ChipKind.Quiet: return Border;
default: return Border;
}
}
}
internal enum ChipKind { Neutral, Info, Ok, Warn, Danger, Quiet }
internal enum IconKind
{
None, ChevronDown, ChevronRight, Check, Bolt, Folder, Doc, ArrowUp, Wrench, Trash, Table, Gear, Sync
}
/// GDI+ 矢量图标:统一 24 网格、约 1.6 描边、圆头。界面内不用 emoji、不用位图。
internal static class Icons
{
public static void Draw(Graphics g, IconKind kind, Rectangle box, Color color)
{
if (kind == IconKind.None || box.Width < 4 || box.Height < 4) return;
var old = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
float side = Math.Min(box.Width, box.Height);
float k = side / 24f;
float ox = box.X + (box.Width - side) / 2f;
float oy = box.Y + (box.Height - side) / 2f;
using (var pen = new Pen(color, Math.Max(1.2f, 1.7f * k)))
{
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;
pen.LineJoin = LineJoin.Round;
switch (kind)
{
case IconKind.ChevronDown:
Poly(g, pen, ox, oy, k, 6, 9, 12, 15, 18, 9);
break;
case IconKind.ChevronRight:
Poly(g, pen, ox, oy, k, 9, 6, 15, 12, 9, 18);
break;
case IconKind.Check:
Poly(g, pen, ox, oy, k, 5, 13, 10, 18, 19, 6);
break;
case IconKind.Bolt:
Poly(g, pen, ox, oy, k, 13, 3, 6, 13, 12, 13, 11, 21, 18, 11, 12, 11, 13, 3);
break;
case IconKind.Folder:
Poly(g, pen, ox, oy, k, 3, 7, 9, 7, 11, 9, 21, 9, 21, 19, 3, 19, 3, 7);
break;
case IconKind.Doc:
Poly(g, pen, ox, oy, k, 6, 3, 15, 3, 19, 7, 19, 21, 6, 21, 6, 3);
Poly(g, pen, ox, oy, k, 15, 3, 15, 7, 19, 7);
Line(g, pen, ox, oy, k, 9, 12, 16, 12);
Line(g, pen, ox, oy, k, 9, 16, 16, 16);
break;
case IconKind.ArrowUp:
Line(g, pen, ox, oy, k, 12, 20, 12, 5);
Poly(g, pen, ox, oy, k, 5, 12, 12, 5, 19, 12);
break;
case IconKind.Wrench:
g.DrawArc(pen, ox + 3 * k, oy + 3 * k, 8 * k, 8 * k, 300f, 250f);
Line(g, pen, ox, oy, k, 10, 14, 19, 21);
break;
case IconKind.Trash:
Poly(g, pen, ox, oy, k, 4, 7, 20, 7);
Poly(g, pen, ox, oy, k, 8, 7, 8, 20, 16, 20, 16, 7);
Line(g, pen, ox, oy, k, 11, 4, 13, 4);
break;
case IconKind.Table:
g.DrawRectangle(pen, ox + 3 * k, oy + 5 * k, 18 * k, 14 * k);
Line(g, pen, ox, oy, k, 3, 10, 21, 10);
Line(g, pen, ox, oy, k, 10, 10, 10, 19);
break;
case IconKind.Gear:
g.DrawEllipse(pen, ox + 8 * k, oy + 8 * k, 8 * k, 8 * k);
g.DrawEllipse(pen, ox + 10.5f * k, oy + 10.5f * k, 3 * k, 3 * k);
break;
case IconKind.Sync: // 拉取服务器数据:带缺口的圆 + 箭头
g.DrawArc(pen, ox + 4 * k, oy + 4 * k, 16 * k, 16 * k, 300f, 250f);
Poly(g, pen, ox, oy, k, 19, 4, 20, 9, 15, 8);
break;
}
}
g.SmoothingMode = old;
}
private static void Poly(Graphics g, Pen pen, float ox, float oy, float k, params float[] xy)
{
var pts = new PointF[xy.Length / 2];
for (int i = 0; i < pts.Length; i++)
pts[i] = new PointF(ox + xy[i * 2] * k, oy + xy[i * 2 + 1] * k);
g.DrawLines(pen, pts);
}
private static void Line(Graphics g, Pen pen, float ox, float oy, float k, float x1, float y1, float x2, float y2)
{
g.DrawLine(pen, ox + x1 * k, oy + y1 * k, ox + x2 * k, oy + y2 * k);
}
}
internal enum BadgeKind { Pending, Active, Done, Warn }
/// 独立编号徽标:用于操作条这类没有卡片头部的区域,与 Card 内的徽标同一套语义。
internal class StepBadge : Control
{
public int Number;
public BadgeKind Kind = BadgeKind.Pending;
public StepBadge()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw
| ControlStyles.SupportsTransparentBackColor, true);
Size = new Size(Theme.Px(22), Theme.Px(22));
BackColor = Color.Transparent;
TabStop = false;
}
public void Set(int number, BadgeKind kind)
{
Number = number;
Kind = kind;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
Color fill, ink;
switch (Kind)
{
case BadgeKind.Active: fill = Theme.Primary; ink = Color.White; break;
case BadgeKind.Done: fill = Theme.Ok; ink = Color.White; break;
case BadgeKind.Warn: fill = Theme.Danger; ink = Color.White; break;
default: fill = Theme.Disabled; ink = Theme.Ink2; break;
}
var old = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
using (var b = new SolidBrush(fill)) g.FillEllipse(b, r);
g.SmoothingMode = old;
if (Kind == BadgeKind.Done) Icons.Draw(g, IconKind.Check, ClientRectangle, ink);
else
TextRenderer.DrawText(g, Number.ToString(), Theme.CaptionBold, ClientRectangle, ink,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding);
}
}
/// 白底圆角承载面板:状态条与操作条这类不需要步骤头部的区域。
/// BackColor 跟着面色走,否则子 Label/CheckBox 会按环境属性继承到灰底。
internal class Surface : Panel
{
private Color _face = Theme.Card;
public Color LineColor = Theme.Border;
public Color FaceColor
{
get { return _face; }
set { _face = value; BackColor = value; Invalidate(); }
}
public Surface()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
BackColor = Theme.Card;
TabStop = false;
}
protected override void OnPaint(PaintEventArgs e)
{
Theme.FillPanel(e.Graphics, new Rectangle(0, 0, Width, Height), _face, LineColor,
Theme.Radius, 1f, Theme.BackdropOf(this));
}
}
///
/// 步骤卡片:编号徽标 + 标题 + 摘要 + 折叠箭头。折叠后只留一行摘要,把高度让给预览表格。
/// 内容子控件由外部按 HeaderH 往下的坐标摆放;展开高度 = HeaderH + BodyH。
///
internal class Card : Panel
{
public int StepNumber;
public string Title = "";
public string Summary = "";
public BadgeKind Badge = BadgeKind.Pending;
public bool Collapsible = true;
public bool Expanded = true;
public bool Invalid = false;
public bool ShowBadge = true;
/// 卡片头部右侧留给按钮的宽度,摘要文字不越过它。
public int HeaderReserve = 0;
public int HeaderH = 44;
public int BodyH = 0;
public Action OnToggle;
private bool _hoverHeader;
public Card()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
BackColor = Theme.Card; // 子 Label 通过环境属性继承白底
TabStop = false;
HeaderH = Theme.Px(44);
}
public Rectangle BodyRect { get { return new Rectangle(0, HeaderH, Width, BodyH); } }
private Rectangle HeaderRect { get { return new Rectangle(0, 0, Width, HeaderH); } }
public void SetSummary(string text)
{
if (Summary == text) return;
Summary = text ?? "";
Invalidate(HeaderRect);
}
public void SetBadge(BadgeKind kind)
{
if (Badge == kind) return;
Badge = kind;
Invalidate(HeaderRect);
}
public void SetInvalid(bool invalid)
{
if (Invalid == invalid) return;
Invalid = invalid;
Invalidate(HeaderRect);
}
public void ApplyHeight()
{
Height = Expanded ? HeaderH + BodyH : HeaderH;
// 折叠时靠父面板裁剪隐藏正文,不去改子控件的 Visible——
// 否则会把「按校验状态自己决定显隐」的行内错误提示强行显示出来。
// 但要把 TabStop 关掉,避免键盘 Tab 进不可见的输入框。
SetTabStop(Controls, Expanded);
Invalidate();
}
/// 输入控件可能嵌在 InputField 容器里,递归切换 TabStop。
private static void SetTabStop(Control.ControlCollection controls, bool enabled)
{
foreach (Control c in controls)
{
if (c is TextBoxBase || c is ComboBox || c is Button || c is CheckBox)
c.TabStop = enabled;
SetTabStop(c.Controls, enabled);
}
}
public void Toggle()
{
Expanded = !Expanded;
if (OnToggle != null) OnToggle(Expanded);
else ApplyHeight();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
bool h = Collapsible && HeaderRect.Contains(e.Location);
if (h != _hoverHeader)
{
_hoverHeader = h;
Cursor = h ? Cursors.Hand : Cursors.Default;
Invalidate(HeaderRect);
}
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
if (_hoverHeader)
{
_hoverHeader = false;
Cursor = Cursors.Default;
Invalidate(HeaderRect);
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (Collapsible && ButtonPressed(e) && HeaderRect.Contains(e.Location)) Toggle();
}
private static bool ButtonPressed(MouseEventArgs e)
{
return e.Button == MouseButtons.Left;
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
var r = new Rectangle(0, 0, Width, Height);
Theme.FillPanel(g, r, Theme.Card,
Invalid ? Theme.Danger : (Expanded ? Theme.PrimarySoftBorder : Theme.Border),
Theme.Radius, Invalid || Expanded ? Theme.HairStrong : Theme.Hair, Theme.BackdropOf(this));
var badge = new Rectangle(Theme.Px(14), (HeaderH - Theme.Px(22)) / 2, Theme.Px(22), Theme.Px(22));
if (ShowBadge)
{
Color badgeFill, badgeInk;
switch (Badge)
{
case BadgeKind.Active: badgeFill = Theme.Primary; badgeInk = Color.White; break;
case BadgeKind.Done: badgeFill = Theme.Ok; badgeInk = Color.White; break;
case BadgeKind.Warn: badgeFill = Theme.Danger; badgeInk = Color.White; break;
default: badgeFill = Theme.Disabled; badgeInk = Theme.Ink2; break;
}
var old = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (var b = new SolidBrush(badgeFill)) g.FillEllipse(b, badge);
g.SmoothingMode = old;
if (Badge == BadgeKind.Done) Icons.Draw(g, IconKind.Check, badge, badgeInk);
else
TextRenderer.DrawText(g, StepNumber.ToString(), Theme.CaptionBold, badge, badgeInk,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding);
}
int tx = ShowBadge ? badge.Right + Theme.Px(10) : Theme.Px(16);
int chevW = (Collapsible ? Theme.Px(30) : 0) + HeaderReserve;
int maxTitle = Math.Max(Theme.Px(40), Width - tx - chevW - Theme.Px(36));
int titleW = Math.Min(Theme.TextWidth(Title, Theme.BodyBold), maxTitle);
var titleRect = new Rectangle(tx, 0, titleW, HeaderH);
TextRenderer.DrawText(g, Title, Theme.BodyBold, titleRect,
Invalid ? Theme.Danger : Theme.Ink,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
int sx = titleRect.Right + Theme.Px(12);
var sumRect = new Rectangle(sx, 0, Math.Max(0, Width - chevW - sx - Theme.Px(12)), HeaderH);
TextRenderer.DrawText(g, Summary, Theme.Caption, sumRect,
Invalid ? Theme.Danger : Theme.Ink2,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
if (Collapsible)
{
var chev = new Rectangle(Width - Theme.Px(30), (HeaderH - Theme.Px(16)) / 2, Theme.Px(16), Theme.Px(16));
Icons.Draw(g, Expanded ? IconKind.ChevronDown : IconKind.ChevronRight, chev,
_hoverHeader ? Theme.Ink : Theme.Ink3);
}
if (Expanded)
using (var p = new Pen(Theme.Border))
g.DrawLine(p, Theme.S16, HeaderH, Width - Theme.S16, HeaderH);
}
}
internal enum BtnKind { Secondary, Primary, Ghost }
/// 扁平按钮:主/次/幽灵三档,悬停与按下只换色不换尺寸;键盘焦点有描边,宽度按内容量出来。
internal class FlatButton : Button
{
public BtnKind Kind = BtnKind.Secondary;
public IconKind Glyph = IconKind.None;
private bool _hover;
private bool _pressed;
public FlatButton()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
Cursor = Cursors.Hand;
Font = Theme.Body;
Height = Theme.BtnH;
Margin = Padding.Empty;
TabStop = true;
UseVisualStyleBackColor = false;
}
/// 按「图标 + 文字 + 左右内边距」定宽,designMin 是 96dpi 下的最小宽度。
public void FitWidth(int designMin)
{
int pad = Theme.Px(13);
int gw = GlyphWidth;
int gap = gw > 0 && !string.IsNullOrEmpty(Text) ? Theme.Px(6) : 0;
Width = Math.Max(Theme.Px(designMin), pad * 2 + gw + gap + Theme.TextWidth(Text, Font));
}
private int GlyphWidth { get { return Glyph == IconKind.None ? 0 : Theme.Px(14); } }
private Color FaceColor
{
get
{
if (!Enabled) return Theme.Disabled;
switch (Kind)
{
case BtnKind.Primary:
return _pressed ? Theme.PrimaryDown : (_hover ? Theme.PrimaryHover : Theme.Primary);
case BtnKind.Ghost:
return _pressed ? Theme.Disabled : (_hover ? Theme.Hover : Theme.Card);
default:
return _pressed ? Theme.TonalDown : (_hover ? Theme.PrimarySoft : Theme.Tonal);
}
}
}
private Color InkColor
{
get
{
if (!Enabled) return Theme.Ink3;
if (Kind == BtnKind.Primary) return Color.White;
return Kind == BtnKind.Ghost ? Theme.Ink2 : Theme.Ink;
}
}
private Color BorderColor
{
get
{
if (!Enabled) return Theme.Border;
if (Kind != BtnKind.Secondary) return Color.Transparent;
return Focused && ShowFocusCues ? Theme.Primary : Theme.TonalBorder;
}
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
var r = new Rectangle(0, 0, Width, Height);
Theme.FillPanel(g, r, FaceColor, BorderColor, Theme.RadiusCtl,
BorderColor == Color.Transparent ? 0f : Theme.Hair, Theme.BackdropOf(this));
string text = Text ?? "";
int tw = Theme.TextWidth(text, Font);
int gw = GlyphWidth;
int gap = gw > 0 && tw > 0 ? Theme.Px(6) : 0;
int x = (Width - (gw + gap + tw)) / 2;
if (gw > 0)
{
Icons.Draw(g, Glyph, new Rectangle(x, (Height - gw) / 2, gw, gw), InkColor);
x += gw + gap;
}
if (tw > 0)
TextRenderer.DrawText(g, text, Font, new Rectangle(x, 0, tw + Theme.Px(2), Height), InkColor,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix);
if (Focused && ShowFocusCues && Enabled)
{
var old = g.SmoothingMode;
var oldOffset = g.PixelOffsetMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.Half;
int inset = Theme.Px(2);
using (var pen = new Pen(Kind == BtnKind.Primary ? Color.White : Theme.Primary, Theme.HairStrong))
using (var p = Theme.RoundRect(new RectangleF(r.X + inset, r.Y + inset,
r.Width - 1 - inset * 2, r.Height - 1 - inset * 2), Math.Max(1, Theme.RadiusCtl - inset)))
{
pen.Alignment = PenAlignment.Inset;
g.DrawPath(pen, p);
}
g.PixelOffsetMode = oldOffset;
g.SmoothingMode = old;
}
}
protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _hover = true; Invalidate(); }
protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _hover = false; _pressed = false; Invalidate(); }
protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); _pressed = true; Invalidate(); }
protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _pressed = false; Invalidate(); }
protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); Invalidate(); }
protected override void OnLostFocus(EventArgs e) { base.OnLostFocus(e); Invalidate(); }
protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }
protected override void OnTextChanged(EventArgs e) { base.OnTextChanged(e); Invalidate(); }
}
/// 状态 chip:圆点 + 文字,宽度自适应。
internal class StatusChip : Control
{
private string _text = "";
private ChipKind _kind = ChipKind.Neutral;
public StatusChip()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw
| ControlStyles.SupportsTransparentBackColor, true);
Font = Theme.Caption;
Height = Theme.Px(24);
BackColor = Color.Transparent;
TabStop = false;
}
public void Set(string text, ChipKind kind)
{
_text = text ?? "";
_kind = kind;
Width = Theme.TextWidth(_text, Font) + Theme.Px(28);
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
var ink = Theme.ChipInk(_kind);
Theme.FillPanel(g, new Rectangle(0, 0, Width, Height),
Theme.ChipFace(_kind), Theme.ChipBorder(_kind), Height / 2, 1f, Theme.BackdropOf(this));
var old = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (var b = new SolidBrush(ink)) g.FillEllipse(b, Theme.Px(9), (Height - Theme.Px(7)) / 2, Theme.Px(7), Theme.Px(7));
g.SmoothingMode = old;
TextRenderer.DrawText(g, _text, Font, new Rectangle(Theme.Px(22), 0, Width - Theme.Px(24), Height), ink,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding | TextFormatFlags.SingleLine);
}
}
/// 细进度条:原生 ProgressBar 在浅色主题里是个带灰框的方块,
/// 换成圆角轨道 + 主色填充,Minimum/Maximum/Value 与原生同名,可直接替换。
internal class ProgressTrack : Control
{
public int Minimum = 0;
public int Maximum = 100;
private int _value;
public int Value
{
get { return _value; }
set
{
int v = Math.Max(Minimum, Math.Min(value, Maximum));
if (v == _value) return;
_value = v;
Invalidate();
}
}
public ProgressTrack()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw
| ControlStyles.SupportsTransparentBackColor, true);
BackColor = Color.Transparent;
TabStop = false;
Height = Theme.Px(8);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
int radius = Math.Max(1, Height / 2);
Theme.FillPanel(g, new Rectangle(0, 0, Width, Height),
Theme.Disabled, Theme.Border, radius, Theme.Hair, Theme.BackdropOf(this));
int span = Math.Max(1, Maximum - Minimum);
int filled = (int)Math.Round((Value - Minimum) * (double)Width / span);
if (filled <= 0) return;
var old = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (var path = Theme.RoundRect(new RectangleF(0, 0, Width - 1f, Height - 1f), radius))
using (var clip = new Region(path))
{
g.SetClip(clip, CombineMode.Replace);
using (var b = new SolidBrush(Theme.Primary)) g.FillRectangle(b, 0, 0, filled, Height);
}
g.ResetClip();
g.SmoothingMode = old;
}
}
/// 圆角输入宿主:装无边框 TextBox/ComboBox,焦点与校验态换描边色。
/// 命名避开 Microsoft.TeamFoundation.WorkItemTracking.Client.Field。
internal class InputField : Panel
{
private bool _invalid;
private ComboBox _combo;
private Chevron _chevron;
// 内容容器:整体内缩 2px 并按圆角 Region 裁剪——恰好让开聚焦态 2px 描边带,
// 否则白底会切掉描边的内半圈,只剩外圈 AA 残边(视觉上就是一圈毛刺)。
// 子控件(尤其被挤出宿主藏箭头的 ComboBox)再怎么越界,也只会被裁在描边圈以内。
private Panel _box;
/// 宿主右侧留给自绘箭头的区域,同时也是点击展开下拉的热区。
private int ArrowZone { get { return Theme.Px(26); } }
public InputField()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
// _box 必须先于 Height 创建:设置高度会触发 OnSizeChanged → LayoutInner
_box = new Panel { BackColor = Theme.Card, TabStop = false };
_box.MouseDown += BoxClicked;
Controls.Add(_box);
BackColor = Theme.Card;
Height = Theme.FieldH;
TabStop = false;
}
/// 点在容器空白处(输入框内边距)时把焦点交给内容控件,与原生输入框手感一致。
private void BoxClicked(object sender, MouseEventArgs e)
{
foreach (Control c in _box.Controls)
{
if (c is TextBoxBase || c is ComboBox) { c.Focus(); break; }
}
}
public bool Invalid
{
get { return _invalid; }
set { if (_invalid != value) { _invalid = value; Invalidate(); } }
}
public T Add(T inner, bool mono) where T : Control
{
var tb = inner as TextBoxBase;
if (tb != null)
{
tb.BorderStyle = BorderStyle.None;
tb.Multiline = false;
}
var cb = inner as ComboBox;
if (cb != null)
{
// net48 的 ComboBox 没有 BorderStyle(.NET Core 才加),只能用 FlatStyle;
// 系统那个灰色箭头方块靠不住(Paint 覆盖不掉),改成把控件加宽挤出容器、
// 由容器 Region 裁掉它并自绘箭头,点击右区展开下拉。
_combo = cb;
cb.FlatStyle = FlatStyle.Flat;
cb.DrawMode = DrawMode.OwnerDrawFixed;
cb.ItemHeight = Theme.Px(22);
cb.DropDown += (s, e) => BeginAlignDropDown();
cb.HandleCreated += (s, e) => HookComboList();
cb.DrawItem += DrawComboItem;
_chevron = new Chevron();
_chevron.MouseDown += (s, e) => { if (e.Button == MouseButtons.Left) ToggleComboDropDown(); };
_box.Controls.Add(_chevron);
}
inner.BackColor = Theme.Card;
inner.Font = mono ? Theme.Mono : Theme.Body;
inner.ForeColor = Theme.Ink;
_box.Controls.Add(inner);
inner.Enter += (s, e) => Invalidate();
inner.Leave += (s, e) => Invalidate();
LayoutInner();
return inner;
}
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); LayoutInner(); }
private void DrawComboItem(object sender, DrawItemEventArgs e)
{
var cb = (ComboBox)sender;
if (e.Index < 0 || e.Index >= cb.Items.Count) return;
bool selected = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
bool inBox = (e.State & DrawItemState.ComboBoxEdit) == DrawItemState.ComboBoxEdit;
var full = new Rectangle(e.Bounds.X, e.Bounds.Y,
Math.Max(e.Bounds.Width, cb.Width - Theme.Px(2)), e.Bounds.Height);
if (inBox)
{
// 闭合态就是普通输入框:白底黑字。焦点/校验态由宿主描边表达,
// 不响应 Selected 标记——否则一聚焦整块变浅蓝,还会把旁边的箭头衬成白色方块。
using (var b = new SolidBrush(Theme.Card)) e.Graphics.FillRectangle(b, full);
TextRenderer.DrawText(e.Graphics, cb.GetItemText(cb.Items[e.Index]), cb.Font,
new Rectangle(full.X, full.Y, Math.Max(0, full.Width - ArrowZone), full.Height),
Theme.Ink,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
return;
}
using (var b = new SolidBrush(selected ? Theme.PrimarySoft : Theme.Card))
e.Graphics.FillRectangle(b, full);
// 列表项贴着自己窗口的左沿,要补一段缩进才和输入框里的文字对齐;
// 闭合态的组合框本身就摆在宿主内缩处,原生 bounds 已经带了这个缩进。
int pad = inBox ? 0 : Theme.Px(10);
int right = inBox ? ArrowZone : Theme.Px(6);
var text = new Rectangle(full.X + pad, full.Y,
Math.Max(0, full.Width - pad - right), full.Height);
TextRenderer.DrawText(e.Graphics, cb.GetItemText(cb.Items[e.Index]), cb.Font, text,
selected ? Theme.Primary : Theme.Ink,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
}
private void LayoutInner()
{
if (_box == null) return; // 构造中途触发 OnSizeChanged 时容器可能还没建好
_box.SetBounds(2, 2, Width - 4, Height - 4);
SetBoxRegion();
foreach (Control c in _box.Controls)
{
if (c is Chevron) continue;
var tb = c as TextBox;
var cb = c as ComboBox;
if (cb != null)
{
// 右端挤出容器、原生箭头由容器 Region 裁掉并由 Chevron 接管;
// 弹出列表的位置与宽度由 ComboListHook 在定位瞬间改写,不受这里加宽影响。
int h = Math.Min(Math.Max(cb.PreferredHeight, Theme.Px(18)), _box.Height - Theme.Px(4));
cb.SetBounds(Theme.Px(7), Math.Max(Theme.Px(2), (_box.Height - h) / 2),
_box.Width - Theme.Px(7) + ArrowZone, h);
continue;
}
int natural = tb != null ? tb.PreferredHeight : Theme.Px(19);
int th = Math.Min(Math.Max(natural, Theme.Px(18)), _box.Height - Theme.Px(4));
c.SetBounds(Theme.Px(7), Math.Max(Theme.Px(2), (_box.Height - th) / 2), _box.Width - Theme.Px(14), th);
}
if (_chevron != null)
{
int size = Theme.Px(18);
_chevron.SetBounds(_box.Width - Theme.Px(7) - size, Theme.Px(2), size, _box.Height - Theme.Px(4));
_chevron.BringToFront(); // 后加的兄弟控件在底层,必须提到组合框上方才看得见
}
Invalidate();
}
/// 圆角 Region 跟着尺寸重建。Region 比容器再内缩 1px:聚焦描边是 2px 内嵌线,
/// AA 过渡会落到距宿主边缘第 3 列上,白底必须留出这条过渡带,否则内沿被切成硬边(毛刺)。
/// 内容控件摆放在第 9 列起,不受这条内缩影响。
private void SetBoxRegion()
{
using (var path = Theme.RoundRect(
new RectangleF(1, 1, _box.Width - 2f, _box.Height - 2f), Math.Max(2, Theme.RadiusCtl - 3)))
{
if (_box.Region != null) _box.Region.Dispose();
_box.Region = new Region(path);
}
}
/// 宿主自绘的下拉箭头,同时是点击展开的热区(系统的灰箭头已被裁掉)。
private class Chevron : Control
{
public Chevron()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw
| ControlStyles.SupportsTransparentBackColor, true);
BackColor = Theme.Card;
TabStop = false;
}
protected override void OnPaint(PaintEventArgs e)
{
int size = Theme.Px(11);
Icons.Draw(e.Graphics, IconKind.ChevronDown,
new Rectangle((Width - size) / 2, (Height - size) / 2, size, size), Theme.Ink2);
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (_combo == null || e.Button != MouseButtons.Left) return;
if (e.X < Width - ArrowZone) return;
ToggleComboDropDown();
}
// net48 没有 ComboBox.Dropped(.NET Core 才有),只能走 CB_SHOWDROPDOWN
private const int CB_GETDROPPEDSTATE = 0x108B;
private const int CB_SHOWDROPDOWN = 0x014F;
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
private void ToggleComboDropDown()
{
if (!_combo.IsHandleCreated) return;
bool dropped = SendMessage(_combo.Handle, CB_GETDROPPEDSTATE, IntPtr.Zero, IntPtr.Zero) != IntPtr.Zero;
SendMessage(_combo.Handle, CB_SHOWDROPDOWN, new IntPtr(dropped ? 0 : 1), IntPtr.Zero);
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct ComboInfo
{
public int cbSize;
public RECT rcItem;
public RECT rcButton;
public int cbState;
public IntPtr hwndCombo;
public IntPtr hwndEdit;
public IntPtr hwndList;
}
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool GetComboBoxInfo(IntPtr hWnd, ref ComboInfo info);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr after,
int x, int y, int cx, int cy, uint flags);
private const uint SWP_NOZORDER = 0x4;
private const uint SWP_NOACTIVATE = 0x10;
private const uint SWP_FRAMECHANGED = 0x20;
private const int GWL_STYLE = -16;
private const int GWL_EXSTYLE = -20;
private const int WS_BORDER = 0x00800000;
private const int WS_EX_CLIENTEDGE = 0x200;
private const int WM_WINDOWPOSCHANGING = 0x0046;
private const uint SWP_NOSIZE_FLAG = 0x1;
private const uint SWP_NOMOVE_FLAG = 0x2;
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct WINDOWPOS
{
public IntPtr hwnd;
public IntPtr hwndInsertAfter;
public int x;
public int y;
public int cx;
public int cy;
public uint flags;
}
/// 挂在弹出列表窗口上,在系统定位的瞬间(WM_WINDOWPOSCHANGING)就把矩形改写成
/// 贴齐宿主——原生代码按组合框自身矩形定位,而组合框为藏箭头是挤出宿主的,
/// 不拦截的话下拉会先出现在偏右偏宽的位置、等 BeginInvoke 再左移,肉眼可见地跳一下。
private sealed class ComboListHook : NativeWindow
{
private readonly InputField _owner;
public ComboListHook(InputField owner) { _owner = owner; }
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_WINDOWPOSCHANGING && _owner != null && _owner.IsHandleCreated)
{
var wp = (WINDOWPOS)System.Runtime.InteropServices.Marshal.PtrToStructure(
m.LParam, typeof(WINDOWPOS));
var host = _owner.RectangleToScreen(_owner.ClientRectangle);
if ((wp.flags & SWP_NOMOVE_FLAG) == 0) { wp.x = host.Left; wp.y = host.Bottom; }
if ((wp.flags & SWP_NOSIZE_FLAG) == 0) wp.cx = host.Width;
System.Runtime.InteropServices.Marshal.StructureToPtr(wp, m.LParam, false);
}
base.WndProc(ref m);
}
}
private ComboListHook _listHook;
/// ComboLBox 随组合框句柄一起创建,尽早挂上钩子,第一次下拉就不跳。
private void HookComboList()
{
if (_listHook != null || _combo == null || !_combo.IsHandleCreated) return;
var info = new ComboInfo();
info.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(ComboInfo));
if (!GetComboBoxInfo(_combo.Handle, ref info) || info.hwndList == IntPtr.Zero) return;
_listHook = new ComboListHook(this);
_listHook.AssignHandle(info.hwndList);
}
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLongW")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "SetWindowLongW")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
/// 把弹出列表重摆到宿主的外沿:列表窗口默认跟着组合框自己的矩形走,
/// 而组合框为了藏原生箭头是加宽挤出宿主的,不重摆就会右移并比输入框宽一截。
/// 顺带剥掉列表窗口的系统边框(WS_BORDER / WS_EX_CLIENTEDGE),
/// 让下拉跟扁平输入框同一套视觉,宽度与宿主完全一致。
/// DropDown 事件里列表窗口还没摆好(原生代码随后才定位),投递到本条消息之后再改。
private void BeginAlignDropDown()
{
if (!IsHandleCreated || _combo == null) return;
BeginInvoke((Action)AlignDropDown);
}
private void AlignDropDown()
{
if (_combo == null || !_combo.IsHandleCreated) return;
HookComboList(); // 兜底:钩子没挂上时先补挂
var info = new ComboInfo();
info.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(ComboInfo));
if (!GetComboBoxInfo(_combo.Handle, ref info) || info.hwndList == IntPtr.Zero) return;
int style = GetWindowLong(info.hwndList, GWL_STYLE);
if ((style & WS_BORDER) != 0)
SetWindowLong(info.hwndList, GWL_STYLE, style & ~WS_BORDER);
int ex = GetWindowLong(info.hwndList, GWL_EXSTYLE);
if ((ex & WS_EX_CLIENTEDGE) != 0)
SetWindowLong(info.hwndList, GWL_EXSTYLE, ex & ~WS_EX_CLIENTEDGE);
RECT r;
if (!GetWindowRect(info.hwndList, out r)) return;
var host = RectangleToScreen(ClientRectangle);
SetWindowPos(info.hwndList, IntPtr.Zero, host.Left, host.Bottom,
host.Width, r.Bottom - r.Top, SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
bool focused = ContainsFocus; // 输入控件嵌在 _box 里,必须递归判断
var color = _invalid ? Theme.Danger : (focused ? Theme.Primary : Theme.BorderStrong);
Theme.FillPanel(g, new Rectangle(0, 0, Width, Height),
Theme.Card, color, Theme.RadiusCtl, focused || _invalid ? Theme.HairStrong : Theme.Hair,
Theme.BackdropOf(this));
}
}
internal enum LogLevel { Info, Ok, Skip, Warn, Fail }
///
/// 分级着色日志控制台。级别按文本内容自动判定,所以 TfsClient 里传进来的 Action<string> 不用改签名。
/// 继承 RichTextBox 但不改其窗口样式:原生控件自绘会破坏滚动与选区。
///
internal class LogView : RichTextBox
{
public bool AutoScrollToBottom = true;
public LogView()
{
ReadOnly = true;
BorderStyle = BorderStyle.None;
BackColor = Theme.LogBg;
ForeColor = Theme.Ink;
Font = Theme.Mono;
DetectUrls = false;
ScrollBars = RichTextBoxScrollBars.Vertical;
Multiline = true;
TabStop = false;
WordWrap = true;
}
public static LogLevel Classify(string msg)
{
if (string.IsNullOrEmpty(msg)) return LogLevel.Info;
if (msg.IndexOf("FAIL", StringComparison.Ordinal) >= 0) return LogLevel.Fail;
if (msg.IndexOf("失败", StringComparison.Ordinal) >= 0) return LogLevel.Fail;
if (msg.IndexOf("错误", StringComparison.Ordinal) >= 0) return LogLevel.Fail;
if (msg.IndexOf("中断", StringComparison.Ordinal) >= 0) return LogLevel.Fail;
if (msg.IndexOf("SKIP", StringComparison.Ordinal) >= 0) return LogLevel.Skip;
if (msg.IndexOf("警告", StringComparison.Ordinal) >= 0) return LogLevel.Warn;
if (msg.IndexOf("不可用", StringComparison.Ordinal) >= 0) return LogLevel.Warn;
if (msg.IndexOf("部分", StringComparison.Ordinal) >= 0) return LogLevel.Warn;
if (msg.IndexOf("] OK", StringComparison.Ordinal) >= 0) return LogLevel.Ok;
if (msg.StartsWith("连接成功", StringComparison.Ordinal)) return LogLevel.Ok;
if (msg.IndexOf("完成", StringComparison.Ordinal) >= 0) return LogLevel.Ok;
if (msg.IndexOf("已加载", StringComparison.Ordinal) >= 0) return LogLevel.Ok;
return LogLevel.Info;
}
private Color InkOf(LogLevel lvl)
{
switch (lvl)
{
case LogLevel.Ok: return Theme.Ok;
case LogLevel.Skip: return Theme.Accent;
case LogLevel.Warn: return Theme.Accent;
case LogLevel.Fail: return Theme.Danger;
default: return Theme.Ink;
}
}
/// 最后一行当前是否可见——可见才跟随滚动,用户往上翻看时不打断。
private bool LastLineVisible()
{
if (TextLength == 0) return true;
int idx = GetCharIndexFromPosition(new Point(0, ClientSize.Height - 4));
return GetLineFromCharIndex(Math.Max(0, idx)) >= GetLineFromCharIndex(TextLength);
}
public void AppendLine(string time, LogLevel lvl, string msg)
{
if (InvokeRequired) { BeginInvoke(new Action(() => AppendLine(time, lvl, msg))); return; }
bool follow = AutoScrollToBottom && LastLineVisible();
SelectionStart = TextLength;
SelectionLength = 0;
SelectionColor = Theme.Ink3;
SelectedText = "[" + time + "] ";
SelectionColor = InkOf(lvl);
SelectedText = msg;
SelectionColor = Theme.Ink;
SelectedText = Environment.NewLine;
if (follow) { SelectionStart = TextLength; ScrollToCaret(); }
}
public void ClearLog()
{
if (InvokeRequired) { BeginInvoke(new Action(ClearLog)); return; }
Clear();
}
}
internal static class GridStyle
{
public static void Apply(DataGridView g)
{
g.BackgroundColor = Theme.Card;
g.BorderStyle = BorderStyle.None;
g.GridColor = Color.FromArgb(0xEE, 0xF1, 0xF6);
g.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal;
g.EnableHeadersVisualStyles = false;
g.RowHeadersVisible = false;
g.AllowUserToAddRows = false;
g.AllowUserToDeleteRows = false;
g.AllowUserToResizeRows = false;
g.ReadOnly = true;
g.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
g.MultiSelect = false;
g.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells;
g.RowTemplate.Height = Theme.Px(28);
g.ColumnHeadersHeight = Theme.Px(32);
g.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
g.DefaultCellStyle.BackColor = Theme.Card;
g.DefaultCellStyle.ForeColor = Theme.Ink;
g.DefaultCellStyle.SelectionBackColor = Theme.PrimarySoft;
g.DefaultCellStyle.SelectionForeColor = Theme.Ink;
g.DefaultCellStyle.Font = Theme.Body;
g.DefaultCellStyle.Padding = new Padding(Theme.Px(6), 0, Theme.Px(6), 0);
g.AlternatingRowsDefaultCellStyle.BackColor = Theme.RowAlt;
g.AlternatingRowsDefaultCellStyle.SelectionBackColor = Theme.PrimarySoft;
g.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(0xF7, 0xF9, 0xFC);
g.ColumnHeadersDefaultCellStyle.ForeColor = Theme.Ink2;
g.ColumnHeadersDefaultCellStyle.Font = Theme.CaptionBold;
g.ColumnHeadersDefaultCellStyle.SelectionBackColor = Color.FromArgb(0xF7, 0xF9, 0xFC);
g.ColumnHeadersDefaultCellStyle.SelectionForeColor = Theme.Ink2;
g.ColumnHeadersDefaultCellStyle.Padding = new Padding(Theme.Px(6), 0, Theme.Px(6), 0);
g.ColumnHeadersDefaultCellStyle.WrapMode = DataGridViewTriState.False;
g.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.None;
}
}
}