Files
chenshoulin ffc210f827 refactor: 移除 CueBanner 类及相关按钮,优化界面布局
- 删除 CueBanner.cs 文件,移除输入框的占位提示功能。
- 更新 MainForm.cs 和 MainFormUi.cs,删除与 Browse 按钮相关的代码,简化用户界面。
- 调整控件布局以提升整体用户体验,确保界面更加简洁。
2026-11-01 23:15:15 +08:00

1593 lines
76 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace TfsExcelImporter
{
/// <summary>
/// 设计 token:颜色、字号、间距、圆角。界面代码只引用这里,不散落具体色值。
/// </summary>
internal static class Theme
{
// --- 缩放 ---
/// <summary>系统缩放系数(96dpi = 1)。必须在 BuildUi 之前按 DeviceDpi 赋值。</summary>
public static float Scale = 1f;
/// <summary>设计稿(96dpi)像素 → 当前缩放下的像素。字体用磅值,Windows 会自动跟缩放,所以只有像素量需要换算。</summary>
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;
}
/// <summary>细描边固定 1 设备像素(跟 Windows 自身控件一致),不随缩放变粗。</summary>
public static float Hair { get { return 1f; } }
/// <summary>强调描边(焦点/校验失败)按缩放取整,150% 下为 2 像素。</summary>
public static float HairStrong { get { return Math.Max(1f, (float)Math.Round(Scale)); } }
/// <summary>填充 + 描边一个圆角面板;borderWidth 为 0 时只填充。
/// Half 像素偏移 + 内嵌画笔让 1px 边线落在整像素上,避免高缩放下糊成双线。
/// 圆角面内切在 r 里,四角和最后 1 行列落在它外面,必须先用 backdrop 铺满整块客户区:
/// 自绘控件的双缓冲只清一次,真实屏幕上那些从未绘制的像素会残留成黑色描边。</summary>
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;
}
/// <summary>控件四周应当露出的底色:优先父容器面色,透明底(自绘宿主)退到卡片白。</summary>
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;
}
private static readonly Dictionary<int, SolidBrush> _eraseBrushes = new Dictionary<int, SolidBrush>();
/// <summary>
/// WM_ERASEBKGND 的快速擦除:直接用 GDI 把客户区铺成底色,不经过双缓冲、不建 GraphicsPath。
/// 自绘控件为了不闪都不擦背景,代价是窗口被重新暴露(还原、切前台)时那块像素保持系统的黑色,
/// 要等完整首帧(实测整窗 100ms+)才盖掉——这就是「黑色界面闪现」的来源。
/// 这里先用一次 FillRect 换掉黑底:即使完整首帧还没来,用户看到的也只是和界面同色的浅底。
/// </summary>
public static bool FastErase(IntPtr hwnd, IntPtr hdc, Color color)
{
RECT rc;
if (color.A == 0 || !GetClientRect(hwnd, out rc) || rc.right <= 0 || rc.bottom <= 0) return false;
int key = color.ToArgb();
SolidBrush brush;
if (!_eraseBrushes.TryGetValue(key, out brush))
{
brush = new SolidBrush(color);
_eraseBrushes[key] = brush;
}
var g = Graphics.FromHdc(hdc);
try { g.FillRectangle(brush, 0, 0, rc.right, rc.bottom); }
finally { g.Dispose(); }
return true;
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int left, top, right, bottom; }
[DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, out RECT rc);
private static readonly Dictionary<string, int> _textWidths = new Dictionary<string, int>();
public static int TextWidth(string text, Font font)
{
if (string.IsNullOrEmpty(text)) return 0;
// 排版会反复量同一批文案(拖拽缩放每帧都要全量量一遍),而 MeasureText 每次都要建 DC、选字体,
// 和查一次字典差两个数量级。字体是静态单例,键里带字体名/字号/字形就足以区分。
string key = font.Name + "|" + font.SizeInPoints.ToString("F1") + "|" + (int)font.Style + "|" + text;
int w;
if (_textWidths.TryGetValue(key, out w)) return w;
w = TextRenderer.MeasureText(text, font, Size.Empty,
TextFormatFlags.NoPadding | TextFormatFlags.SingleLine).Width;
if (_textWidths.Count > 800) _textWidths.Clear(); // 日志摘要/路径是动态文案,别让它无限涨
_textWidths[key] = w;
return w;
}
/// <summary>
/// 位置尺寸都没变就不动。每次 SetBounds 都是一次 WM_WINDOWPOSCHANGED 加一次重绘,
/// 而重排里绝大多数控件其实待在原地——打字、状态刷新这类重排尤其如此,
/// 拦掉空转会把这些场景的 70~100ms 直接抹平。
/// </summary>
public static void Place(Control c, int x, int y, int w, int h)
{
var b = c.Bounds;
if (b.X == x && b.Y == y && b.Width == w && b.Height == h) return;
c.SetBounds(x, y, w, h);
}
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, Fetch, Tree
}
/// <summary>
/// GDI+ 矢量图标:统一 24 网格、约 2 描边、圆头圆角。界面内不用 emoji、不用位图。
/// 这条流水线的图标实际只有 14~16px,所以几何一律「少笔画、留大间隙」:
/// 细节(桶身竖纹、齿轮齿)在小尺寸会糊成一坨,宁可省略也不用 2 个网格以内的短笔画。
/// </summary>
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;
// 描边随尺寸线性走(24 网格 2px),最小 1.35 保证 14px 下不断线
float stroke = Math.Max(1.35f, 2f * k);
using (var pen = new Pen(color, stroke))
using (var brush = new SolidBrush(color))
{
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;
pen.LineJoin = LineJoin.Round;
switch (kind)
{
case IconKind.ChevronDown:
Poly(g, pen, ox, oy, k, 6.2f, 9.2f, 12f, 15f, 17.8f, 9.2f);
break;
case IconKind.ChevronRight:
Poly(g, pen, ox, oy, k, 9.2f, 6.2f, 15f, 12f, 9.2f, 17.8f);
break;
case IconKind.Check:
Poly(g, pen, ox, oy, k, 4.8f, 12.6f, 9.6f, 17.4f, 19.2f, 6.6f);
break;
case IconKind.Bolt: // 实心:14px 下描边闪电会糊成一根棍
Fill(g, brush, ox, oy, k, 13.2f, 2.4f, 4.8f, 13.4f, 10.4f, 13.4f,
10.8f, 21.6f, 19.2f, 10.6f, 13.6f, 10.6f);
break;
case IconKind.Folder:
Poly(g, pen, ox, oy, k, 2.8f, 6.4f, 9.4f, 6.4f, 11.4f, 8.6f,
21.2f, 8.6f, 21.2f, 18.8f, 2.8f, 18.8f, 2.8f, 6.4f);
break;
case IconKind.Doc:
Poly(g, pen, ox, oy, k, 5.8f, 3f, 14.6f, 3f, 19f, 7.4f, 19f, 21f, 5.8f, 21f, 5.8f, 3f);
Poly(g, pen, ox, oy, k, 14.6f, 3f, 14.6f, 7.4f, 19f, 7.4f);
Line(g, pen, ox, oy, k, 8.8f, 12.2f, 16f, 12.2f);
Line(g, pen, ox, oy, k, 8.8f, 16.2f, 16f, 16.2f);
break;
case IconKind.ArrowUp:
Line(g, pen, ox, oy, k, 12f, 20.4f, 12f, 5.2f);
Poly(g, pen, ox, oy, k, 5.8f, 11.4f, 12f, 5.2f, 18.2f, 11.4f);
break;
case IconKind.Wrench: // 实心开口扳手:环+手柄在 16px 下会读成放大镜,实心剪影才立得住
var wst = g.Save();
g.TranslateTransform(ox + 12f * k, oy + 12f * k);
g.RotateTransform(45f);
using (var path = new GraphicsPath())
{
path.AddPolygon(new[]
{
new PointF(-4.8f * k, -9.4f * k), new PointF(-1.7f * k, -9.4f * k),
new PointF(-1.7f * k, -6.2f * k), new PointF(1.7f * k, -6.2f * k),
new PointF(1.7f * k, -9.4f * k), new PointF(4.8f * k, -9.4f * k),
new PointF(4.8f * k, -4.4f * k), new PointF(2.2f * k, -1.6f * k),
new PointF(2.2f * k, 9.6f * k), new PointF(-2.2f * k, 9.6f * k),
new PointF(-2.2f * k, -1.6f * k), new PointF(-4.8f * k, -4.4f * k)
});
g.FillPath(brush, path);
}
g.Restore(wst);
break;
case IconKind.Trash:
Line(g, pen, ox, oy, k, 4.2f, 8.2f, 19.8f, 8.2f); // 盖
Line(g, pen, ox, oy, k, 9.4f, 4.2f, 14.6f, 4.2f); // 提手(与盖留 4 单位空隙)
Poly(g, pen, ox, oy, k, 6.8f, 8.2f, 6.8f, 20f, 17.2f, 20f, 17.2f, 8.2f); // 桶身
break;
case IconKind.Table:
var tr = new RectangleF(ox + 2.8f * k, oy + 4.6f * k, 18.4f * k, 14.8f * k);
using (var path = Theme.RoundRect(tr, 2.2f * k)) g.DrawPath(pen, path);
Line(g, pen, ox, oy, k, 2.8f, 9.6f, 21.2f, 9.6f);
Line(g, pen, ox, oy, k, 10.4f, 9.6f, 10.4f, 19.4f);
break;
case IconKind.Gear: // 环 + 8 根短齿 + 实心轴:齿短一点才不会糊成太阳
for (int i = 0; i < 8; i++)
{
double a = i * Math.PI / 4;
Line(g, pen, ox, oy, k, 12f + (float)Math.Cos(a) * 5.2f, 12f + (float)Math.Sin(a) * 5.2f,
12f + (float)Math.Cos(a) * 7.4f, 12f + (float)Math.Sin(a) * 7.4f);
}
g.DrawEllipse(pen, ox + 6.8f * k, oy + 6.8f * k, 10.4f * k, 10.4f * k);
g.FillEllipse(brush, ox + 10.2f * k, oy + 10.2f * k, 3.6f * k, 3.6f * k);
break;
case IconKind.Fetch: // 从服务器拉取(加载迭代列表):下箭头落进托盘,与「开始导入」的上箭头成对
// 圆形箭头在 16px 下始终读成「缺口的圈 + 一坨」,这里换直线几何;
// 托盘底边取 19.8 而不是 20.2,100%/125% 下都落在像素中心,不会渲成一条虚灰线
Line(g, pen, ox, oy, k, 12f, 3.4f, 12f, 12.2f);
Poly(g, pen, ox, oy, k, 6.4f, 7.4f, 12f, 13.2f, 17.6f, 7.4f);
Poly(g, pen, ox, oy, k, 5f, 14.6f, 5f, 19.8f, 19f, 19.8f, 19f, 14.6f);
break;
case IconKind.Tree: // 父项 + 两个子项:比链环更像「挂到父项下」,16px 也分得清
using (var node = Theme.RoundRect(new RectangleF(ox + 8.8f * k, oy + 2.6f * k, 6.4f * k, 5.2f * k), 1.4f * k))
using (var c1 = Theme.RoundRect(new RectangleF(ox + 2.6f * k, oy + 15.4f * k, 6.4f * k, 5.2f * k), 1.4f * k))
using (var c2 = Theme.RoundRect(new RectangleF(ox + 15f * k, oy + 15.4f * k, 6.4f * k, 5.2f * k), 1.4f * k))
{
g.FillPath(brush, node);
g.FillPath(brush, c1);
g.FillPath(brush, c2);
}
Line(g, pen, ox, oy, k, 12f, 7.8f, 12f, 12.4f);
Line(g, pen, ox, oy, k, 5.8f, 12.4f, 18.2f, 12.4f);
Line(g, pen, ox, oy, k, 5.8f, 12.4f, 5.8f, 15.4f);
Line(g, pen, ox, oy, k, 18.2f, 12.4f, 18.2f, 15.4f);
break;
}
}
g.SmoothingMode = old;
}
/// <summary>圆徽标里画勾用的内缩盒子:铺满整个圆会贴边,视觉上像被裁掉一块。</summary>
public static Rectangle BadgeGlyph(Rectangle circle)
{
int inset = Math.Max(2, circle.Width * 2 / 11); // 22px 圆 → 4px 内缩(14px 盒子)
return Rectangle.Inflate(circle, -inset, -inset);
}
private static void Poly(Graphics g, Pen pen, float ox, float oy, float k, params float[] xy)
{
g.DrawLines(pen, Points(ox, oy, k, xy));
}
private static void Fill(Graphics g, Brush brush, float ox, float oy, float k, params float[] xy)
{
g.FillPolygon(brush, Points(ox, oy, k, xy));
}
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);
}
private static PointF[] Points(float ox, float oy, float k, 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);
return pts;
}
}
internal enum BadgeKind { Pending, Active, Done, Warn }
/// <summary>独立编号徽标:用于操作条这类没有卡片头部的区域,与 Card 内的徽标同一套语义。</summary>
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);
if (Kind == BadgeKind.Pending)
using (var p = new Pen(Theme.BorderStrong, Theme.Hair)) g.DrawEllipse(p, r);
g.SmoothingMode = old;
// 勾画在圆内缩 4px(14px 盒子 / 22px 圆),铺满整圆会贴边看着像被裁掉
if (Kind == BadgeKind.Done) Icons.Draw(g, IconKind.Check, Icons.BadgeGlyph(ClientRectangle), ink);
else
TextRenderer.DrawText(g, Number.ToString(), Theme.CaptionBold, ClientRectangle, ink,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding);
}
}
/// <summary>白底圆角承载面板:状态条与操作条这类不需要步骤头部的区域。
/// BackColor 跟着面色走,否则子 Label/CheckBox 会按环境属性继承到灰底。</summary>
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 WndProc(ref Message m)
{
if (m.Msg == 0x0014 && Theme.FastErase(m.HWnd, m.WParam, BackColor))
{
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
}
protected override void OnPaint(PaintEventArgs e)
{
Theme.FillPanel(e.Graphics, new Rectangle(0, 0, Width, Height), _face, LineColor,
Theme.Radius, 1f, Theme.BackdropOf(this));
}
}
/// <summary>
/// 步骤卡片:编号徽标 + 标题 + 摘要 + 折叠箭头。折叠后只留一行摘要,把高度让给预览表格。
/// 内容子控件由外部按 HeaderH 往下的坐标摆放;展开高度 = HeaderH + BodyH。
/// </summary>
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;
/// <summary>卡片头部右侧留给按钮的宽度,摘要文字不越过它。</summary>
public int HeaderReserve = 0;
/// <summary>头部摘要按「圆点 + 短语」分段绘制(状态摘要样式);默认纯文本。</summary>
public bool SummaryDots = false;
public int HeaderH = 44;
public int BodyH = 0;
public Action<bool> 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);
}
// 卡片占了大半客户区,窗口重新暴露时它不擦背景就是一大片黑——和 MainForm 一样先铺面色。
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x0014 && Theme.FastErase(m.HWnd, m.WParam, BackColor))
{
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
}
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();
}
/// <summary>输入控件可能嵌在 InputField 容器里,递归切换 TabStop。</summary>
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);
// 未激活徽章加一圈浅描边:纯灰底在白卡上存在感太弱,描边后与「激活/完成」的实心徽章同属一套语义
if (Badge == BadgeKind.Pending)
using (var p = new Pen(Theme.BorderStrong, Theme.Hair))
g.DrawEllipse(p, badge.X + 0.5f, badge.Y + 0.5f, badge.Width - 1f, badge.Height - 1f);
g.SmoothingMode = old;
if (Badge == BadgeKind.Done) Icons.Draw(g, IconKind.Check, Icons.BadgeGlyph(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);
int sumW = Math.Max(0, Width - chevW - sx - Theme.Px(12));
if (SummaryDots)
DrawDotSummary(g, Summary, sx, sumW);
else
TextRenderer.DrawText(g, Summary, Theme.Caption,
new Rectangle(sx, 0, sumW, HeaderH),
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);
}
/// <summary>「圆点 + 短语」状态摘要:按 " · " 分段,每段一枚圆点;放不下的段整段省略。</summary>
private void DrawDotSummary(Graphics g, string summary, int x, int availW)
{
if (string.IsNullOrEmpty(summary) || availW <= 0) return;
var segments = summary.Split(new[] { " · " }, StringSplitOptions.None);
int dot = Theme.Px(6);
int dotGap = Theme.Px(6);
int segGap = Theme.Px(14);
int cursor = x;
int limit = x + availW;
foreach (var segment in segments)
{
if (cursor + dot + dotGap > limit) break;
var oldSmoothing = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (var b = new SolidBrush(Invalid ? Theme.Danger : Theme.Ink3))
g.FillEllipse(b, cursor, (HeaderH - dot) / 2, dot, dot);
g.SmoothingMode = oldSmoothing;
cursor += dot + dotGap;
int remain = limit - cursor;
if (remain <= 0) break;
int tw = Theme.TextWidth(segment, Theme.Caption);
bool truncated = tw > remain;
var flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine;
if (truncated) flags |= TextFormatFlags.EndEllipsis;
TextRenderer.DrawText(g, segment, Theme.Caption,
new Rectangle(cursor, 0, Math.Min(tw + Theme.Px(2), remain), HeaderH),
Invalid ? Theme.Danger : Theme.Ink2, flags);
cursor += Math.Min(tw, remain) + segGap;
if (truncated) break;
}
}
}
internal enum BtnKind { Secondary, Primary, Ghost }
/// <summary>扁平按钮:主/次/幽灵三档,悬停与按下只换色不换尺寸;键盘焦点有描边,宽度按内容量出来。</summary>
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;
}
/// <summary>按「图标 + 文字 + 左右内边距」定宽,designMin 是 96dpi 下的最小宽度。</summary>
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(16); } }
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(); }
}
/// <summary>状态 chip:圆点 + 文字,宽度自适应。</summary>
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);
}
}
/// <summary>细进度条:原生 ProgressBar 在浅色主题里是个带灰框的方块,
/// 换成圆角轨道 + 主色填充,Minimum/Maximum/Value 与原生同名,可直接替换。</summary>
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;
}
}
/// <summary>圆角输入宿主:装无边框 TextBox/ComboBox,焦点与校验态换描边色。
/// 命名避开 Microsoft.TeamFoundation.WorkItemTracking.Client.Field。</summary>
internal class InputField : Panel
{
private bool _invalid;
private ComboBox _combo;
private Chevron _chevron;
private SuffixIcon _suffix;
private Control _content; // 主内容控件(TextBox/ComboBox),占位层据此判定显隐
private CueLabel _cue; // 自绘占位提示:原生 EM_SETCUEBANNER 控制不了颜色/字号/对齐
// 内容容器:整体内缩 2px 并按圆角 Region 裁剪——恰好让开聚焦态 2px 描边带,
// 否则白底会切掉描边的内半圈,只剩外圈 AA 残边(视觉上就是一圈毛刺)。
// 子控件(尤其被挤出宿主藏箭头的 ComboBox)再怎么越界,也只会被裁在描边圈以内。
private Panel _box;
/// <summary>上一次 LayoutInner 时的宿主尺寸:尺寸没变就不用重摆子控件、更不用重建圆角 Region。</summary>
private Size _innerSize = new Size(-1, -1);
/// <summary>宿主右侧留给自绘箭头的区域,同时也是点击展开下拉的热区。</summary>
private int ArrowZone { get { return Theme.Px(26); } }
/// <summary>内容右侧的固定预留:组合框是箭头热区,文本框是尾部图标按钮(没有则 0)。</summary>
private int RightReserve
{
get { return _chevron != null ? ArrowZone : (_suffix != null ? Theme.Px(26) : 0); }
}
/// <summary>内容左缘相对 _box 的缩进。TextBox 就摆在 Px(7),组合框闭合态文字必须共用同一基准,
/// 否则同一排的输入框文字会错开一条竖线。</summary>
private static int TextInsetX { get { return Theme.Px(7); } }
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;
}
/// <summary>点在容器空白处(输入框内边距)时把焦点交给内容控件,与原生输入框手感一致。</summary>
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>(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);
}
_content = inner;
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(true);
return inner;
}
/// <summary>文本框尾部图标按钮(如 Excel 字段的「浏览」):点击执行回调,悬停着色提示可点。</summary>
public void AddSuffix(IconKind glyph, Action onClick)
{
if (_suffix != null) return;
_suffix = new SuffixIcon { Glyph = glyph };
_suffix.Click += (s, e) => { if (onClick != null) onClick(); };
_box.Controls.Add(_suffix);
LayoutInner(true);
}
/// <summary>忙等时独立禁用尾部按钮,不影响输入框本身的编辑。</summary>
public void SetSuffixEnabled(bool enabled)
{
if (_suffix != null) _suffix.Enabled = enabled;
}
/// <summary>
/// 自绘占位提示:比原生 EM_SETCUEBANNER 多出「浅灰、小字号、水平居中」三件事。
/// 鼠标经 HTTRANSPARENT 穿透到输入框,文字随内容输入即时显隐,与 Web placeholder 手感一致。
/// </summary>
public void SetCue(string text)
{
if (_cue == null)
{
_cue = new CueLabel();
_box.Controls.Add(_cue);
}
_cue.Text = text ?? "";
if (_content != null) _content.TextChanged += (s, e) => UpdateCueVisibility();
UpdateCueVisibility();
LayoutInner(true);
}
private void UpdateCueVisibility()
{
if (_cue != null && _content != null)
_cue.Visible = string.IsNullOrEmpty(_content.Text);
}
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);
// 文字矩形不能直接用 e.Bounds:原生闭合区自带内缩(左侧约 2px、上沿同理),
// 且该内缩随 DPI 放大(150% 下实测 4px),照它画出来的文字会比同排 TextBox
// 右移、下沉 3~4px,看上去就是「没居中」。改成在 _box 坐标里定目标矩形
// (与 TextBox 共用 TextInsetX 基准),再换算到组合框客户区,DPI 无关。
// 额外再让 1px:原生那条「闭合区」裁剪边界正好压在文字起点上,贴着画会把首字
// 最左边的反锯齿列切掉("ntlm" 的 n 少半竖就是这个);顺带补上 TextBox 自身
// 1px 的 EM 内边距,两者文字才是同一条竖线。
var target = new Rectangle(TextInsetX + Theme.Px(1), 0,
Math.Max(0, _box.Width - TextInsetX - Theme.Px(1) - ArrowZone), _box.Height);
var textRect = cb.IsHandleCreated && _box.IsHandleCreated
? cb.RectangleToClient(_box.RectangleToScreen(target))
: target;
TextRenderer.DrawText(e.Graphics, cb.GetItemText(cb.Items[e.Index]), cb.Font,
textRect, 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);
// 列表项贴着自己窗口的左沿(列表窗口对齐宿主外沿),要补上 _box 的 2px 内缩
// 和文字基准缩进,才和闭合态里的文字严格同一条竖线。
int pad = TextInsetX + 2;
int right = 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(bool force = false)
{
if (_box == null) return; // 构造中途触发 OnSizeChanged 时容器可能还没建好
// 尺寸没变 → 子控件位置也不会变,直接跳过:这里一次要重摆 3~5 个真实子窗并重建 Region,
// 单次 3~4ms,重排里被反复调用的正是这种空转。
var hostSize = new Size(Width, Height);
if (!force && hostSize == _innerSize) return;
_innerSize = hostSize;
_box.SetBounds(2, 2, Width - 4, Height - 4);
SetBoxRegion();
foreach (Control c in _box.Controls)
{
if (c is Chevron || c is SuffixIcon || c is CueLabel) 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));
// Edit 自绘时把文字画在客户区上沿往下 Px(2) 处,而 PreferredHeight 底部还含着约
// Px(4) 的边框留白(BorderStyle=None 也不减)。直接按 PreferredHeight 居中,等于把
// 「上留白 2 + 文字 + 下留白 4」整体居中——文字就偏上 Px(2),肉眼一眼能看出来。
// 把这段底部留白的一半补回来,文字才落在框内视觉中线上(Consolas / 雅黑都适用)。
int textTopFix = tb != null ? Theme.Px(2) : 0;
c.SetBounds(Theme.Px(7), Math.Max(Theme.Px(2), (_box.Height - th) / 2) + textTopFix,
_box.Width - Theme.Px(14) - RightReserve, 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(); // 后加的兄弟控件在底层,必须提到组合框上方才看得见
}
if (_suffix != null)
{
int size = Theme.Px(16);
_suffix.SetBounds(_box.Width - Theme.Px(8) - size, Theme.Px(2), size, _box.Height - Theme.Px(4));
}
if (_cue != null)
{
// 与内容文字同一缩进基准,宽度让开尾部箭头/图标区;文字左对齐(不再居中)。
// 起点就用 TextInsetX:实测(150% 截图)空态占位与已填内容的第一个墨迹列
// 相对左边框都在 +13px,多让或让少都会在「敲字瞬间」看到文字横跳。
_cue.SetBounds(TextInsetX, Theme.Px(2),
Math.Max(Theme.Px(24), _box.Width - TextInsetX - Theme.Px(7) - RightReserve),
_box.Height - Theme.Px(4));
_cue.BringToFront();
}
Invalidate();
}
/// <summary>圆角 Region 跟着尺寸重建。Region 比容器再内缩 1px:聚焦描边是 2px 内嵌线,
/// AA 过渡会落到距宿主边缘第 3 列上,白底必须留出这条过渡带,否则内沿被切成硬边(毛刺)。
/// 内容控件摆放在第 9 列起,不受这条内缩影响。</summary>
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);
}
}
/// <summary>宿主自绘的下拉箭头,同时是点击展开的热区(系统的灰箭头已被裁掉)。</summary>
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);
}
}
/// <summary>输入框尾部图标按钮:白底上悬停变主色,点击交给外部回调。</summary>
private sealed class SuffixIcon : Control
{
public IconKind Glyph = IconKind.None;
private bool _hover;
public SuffixIcon()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw
| ControlStyles.SupportsTransparentBackColor, true);
BackColor = Theme.Card;
Cursor = Cursors.Hand;
TabStop = false;
}
protected override void OnMouseEnter(EventArgs e) { base.OnMouseEnter(e); _hover = true; Invalidate(); }
protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); _hover = false; Invalidate(); }
protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); Invalidate(); }
protected override void OnPaint(PaintEventArgs e)
{
int size = Theme.Px(16);
Icons.Draw(e.Graphics, Glyph,
new Rectangle((Width - size) / 2, (Height - size) / 2, size, size),
_hover && Enabled ? Theme.Primary : Theme.Ink2);
}
}
/// <summary>占位提示层:小一号字号、置灰、左对齐并贴住真实文字的左缘;鼠标事件穿透,不挡输入框聚焦与编辑。</summary>
private sealed class CueLabel : Control
{
public CueLabel()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
BackColor = Theme.Card;
TabStop = false;
}
protected override void WndProc(ref Message m)
{
const int WM_NCHITTEST = 0x84;
if (m.Msg == WM_NCHITTEST) { m.Result = (IntPtr)(-1); return; } // HTTRANSPARENT:点击穿透
base.WndProc(ref m);
}
protected override void OnPaint(PaintEventArgs e)
{
// 左对齐:占位文字必须从真实文字的起点起头,用户一敲字两个位置才不跳动。
// NoPadding 去掉 DrawText 自带的 2px 文字边距,起点完全由外层给的 x 决定。
TextRenderer.DrawText(e.Graphics, Text, Theme.Caption,
new Rectangle(0, 0, Width, Height), Theme.Ink3,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
}
}
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;
// 先让组合框拿到焦点:没焦点就 CB_SHOWDROPDOWN,列表展开后随即被焦点变化收掉,
// 表现出来就是「点一下闪一下」。
if (!_combo.Focused) _combo.Focus();
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;
}
/// <summary>挂在弹出列表窗口上,在系统定位的瞬间(WM_WINDOWPOSCHANGING)就把矩形改写成
/// 贴齐宿主——原生代码按组合框自身矩形定位,而组合框为藏箭头是挤出宿主的,
/// 不拦截的话下拉会先出现在偏右偏宽的位置、等 BeginInvoke 再左移,肉眼可见地跳一下。</summary>
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;
private IntPtr _listHwnd = IntPtr.Zero;
private bool _listSkinned;
/// <summary>ComboLBox 随组合框句柄一起创建,尽早挂上钩子,第一次下拉就不跳。</summary>
private void HookComboList()
{
if (_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;
if (_listHook != null && _listHook.Handle == info.hwndList) return; // 已挂在当前列表窗口上
// 列表窗口被重建过:旧钩子会留在已销毁的句柄上,必须换新的
if (_listHook != null) { _listHook.ReleaseHandle(); _listHook = null; }
_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);
/// <summary>把弹出列表重摆到宿主的外沿:列表窗口默认跟着组合框自己的矩形走,
/// 而组合框为了藏原生箭头是加宽挤出宿主的,不重摆就会右移并比输入框宽一截。
/// 顺带剥掉列表窗口的系统边框(WS_BORDER / WS_EX_CLIENTEDGE),
/// 让下拉跟扁平输入框同一套视觉,宽度与宿主完全一致。</summary>
/// <summary>DropDown 事件里列表窗口还没摆好(原生代码随后才定位),投递到本条消息之后再改。</summary>
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;
// 列表窗口重建后边框会长回来,按句柄判断是否需要重新剥一次
if (_listHwnd != info.hwndList) { _listHwnd = info.hwndList; _listSkinned = false; }
bool skin = false;
if (!_listSkinned)
{
int style = GetWindowLong(info.hwndList, GWL_STYLE);
if ((style & WS_BORDER) != 0)
{
SetWindowLong(info.hwndList, GWL_STYLE, style & ~WS_BORDER);
skin = true;
}
int ex = GetWindowLong(info.hwndList, GWL_EXSTYLE);
if ((ex & WS_EX_CLIENTEDGE) != 0)
{
SetWindowLong(info.hwndList, GWL_EXSTYLE, ex & ~WS_EX_CLIENTEDGE);
skin = true;
}
_listSkinned = true;
}
RECT r;
if (!GetWindowRect(info.hwndList, out r)) return;
var host = RectangleToScreen(ClientRectangle);
// 位置已经被 ComboListHook 改写到宿主外沿时不要再 SetWindowPos:
// 每重摆一次列表窗口都要整窗重算重画,肉眼就是「打开瞬间闪一下」。
bool moved = r.Left != host.Left || r.Top != host.Bottom || (r.Right - r.Left) != host.Width;
if (!moved && !skin) return;
// 只有真的改了窗口样式才需要 SWP_FRAMECHANGED(它强制整窗重算非客户区)
SetWindowPos(info.hwndList, IntPtr.Zero, host.Left, host.Bottom,
host.Width, Math.Max(0, r.Bottom - r.Top),
SWP_NOZORDER | SWP_NOACTIVATE | (skin ? SWP_FRAMECHANGED : 0));
}
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 }
/// <summary>
/// 分级着色日志控制台。级别按文本内容自动判定,所以 TfsClient 里传进来的 Action&lt;string&gt; 不用改签名。
/// 继承 RichTextBox 但不改其窗口样式:原生控件自绘会破坏滚动与选区。
/// </summary>
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;
}
}
/// <summary>最后一行当前是否可见——可见才跟随滚动,用户往上翻看时不打断。</summary>
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;
}
}
}