fix: 更新图标和按钮样式以提升用户界面一致性

- 在 MainFormUi.cs 中,将“加载迭代列表”按钮的图标更改为 Fetch,更新“补挂父项”按钮的图标为 Tree,以增强视觉一致性。
- 在 Theme.cs 中,新增 Fetch 和 Tree 图标类型,并优化现有图标的绘制逻辑,确保在不同尺寸下的清晰度。
- debug.log 文件中添加了新的错误记录,便于后续调试和问题追踪。
This commit is contained in:
chenshoulin
2026-11-01 19:34:17 +08:00
parent dbe8ff2c08
commit afed57047a
3 changed files with 164 additions and 53 deletions
+2 -2
View File
@@ -276,7 +276,7 @@ namespace TfsExcelImporter
_fExcel = NewField(_txtExcel, 500, true); _fExcel = NewField(_txtExcel, 500, true);
_fParent = NewField(_txtParentPbi, 150, true); _fParent = NewField(_txtParentPbi, 150, true);
_btnLoadIterations = NewButton("加载迭代列表", BtnKind.Secondary, IconKind.Sync, 130); _btnLoadIterations = NewButton("加载迭代列表", BtnKind.Secondary, IconKind.Fetch, 130);
_btnBrowse = NewButton("浏览…", BtnKind.Secondary, IconKind.Folder, 92); _btnBrowse = NewButton("浏览…", BtnKind.Secondary, IconKind.Folder, 92);
_btnTemplate = NewButton("导出 Excel 模板", BtnKind.Secondary, IconKind.Doc, 142); _btnTemplate = NewButton("导出 Excel 模板", BtnKind.Secondary, IconKind.Doc, 142);
_chkSkip = NewCheck("跳过已存在标题", ""); _chkSkip = NewCheck("跳过已存在标题", "");
@@ -454,7 +454,7 @@ namespace TfsExcelImporter
_cardAdv.ShowBadge = false; _cardAdv.ShowBadge = false;
_fArea = NewField(_txtAreaPath, 420, true); _fArea = NewField(_txtAreaPath, 420, true);
_btnFixArea = NewButton("按迭代修 Area", BtnKind.Secondary, IconKind.Wrench, 158); _btnFixArea = NewButton("按迭代修 Area", BtnKind.Secondary, IconKind.Wrench, 158);
_btnLinkParent = NewButton("补挂父项", BtnKind.Secondary, IconKind.ArrowUp, 120); _btnLinkParent = NewButton("补挂父项", BtnKind.Secondary, IconKind.Tree, 120);
int y = _cardAdv.HeaderH + Theme.S12; int y = _cardAdv.HeaderH + Theme.S12;
y = Row(_cardAdv, y, 52, "Area 路径", _fArea, "改 Area 与补挂父项都按上面的「目标迭代」筛选已有工作项"); y = Row(_cardAdv, y, 52, "Area 路径", _fArea, "改 Area 与补挂父项都按上面的「目标迭代」筛选已有工作项");
+160 -51
View File
@@ -199,10 +199,14 @@ namespace TfsExcelImporter
internal enum IconKind internal enum IconKind
{ {
None, ChevronDown, ChevronRight, Check, Bolt, Folder, Doc, ArrowUp, Wrench, Trash, Table, Gear, Sync None, ChevronDown, ChevronRight, Check, Bolt, Folder, Doc, ArrowUp, Wrench, Trash, Table, Gear, Fetch, Tree
} }
/// <summary>GDI+ 矢量图标:统一 24 网格、约 1.6 描边、圆头。界面内不用 emoji、不用位图。</summary> /// <summary>
/// GDI+ 矢量图标:统一 24 网格、约 2 描边、圆头圆角。界面内不用 emoji、不用位图。
/// 这条流水线的图标实际只有 14~16px,所以几何一律「少笔画、留大间隙」:
/// 细节(桶身竖纹、齿轮齿)在小尺寸会糊成一坨,宁可省略也不用 2 个网格以内的短笔画。
/// </summary>
internal static class Icons internal static class Icons
{ {
public static void Draw(Graphics g, IconKind kind, Rectangle box, Color color) public static void Draw(Graphics g, IconKind kind, Rectangle box, Color color)
@@ -215,7 +219,10 @@ namespace TfsExcelImporter
float ox = box.X + (box.Width - side) / 2f; float ox = box.X + (box.Width - side) / 2f;
float oy = box.Y + (box.Height - side) / 2f; float oy = box.Y + (box.Height - side) / 2f;
using (var pen = new Pen(color, Math.Max(1.2f, 1.7f * k))) // 描边随尺寸线性走(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.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round; pen.EndCap = LineCap.Round;
@@ -223,69 +230,127 @@ namespace TfsExcelImporter
switch (kind) switch (kind)
{ {
case IconKind.ChevronDown: case IconKind.ChevronDown:
Poly(g, pen, ox, oy, k, 6, 9, 12, 15, 18, 9); Poly(g, pen, ox, oy, k, 6.2f, 9.2f, 12f, 15f, 17.8f, 9.2f);
break; break;
case IconKind.ChevronRight: case IconKind.ChevronRight:
Poly(g, pen, ox, oy, k, 9, 6, 15, 12, 9, 18); Poly(g, pen, ox, oy, k, 9.2f, 6.2f, 15f, 12f, 9.2f, 17.8f);
break; break;
case IconKind.Check: case IconKind.Check:
Poly(g, pen, ox, oy, k, 5, 13, 10, 18, 19, 6); Poly(g, pen, ox, oy, k, 4.8f, 12.6f, 9.6f, 17.4f, 19.2f, 6.6f);
break; break;
case IconKind.Bolt: case IconKind.Bolt: // 实心:14px 下描边闪电会糊成一根棍
Poly(g, pen, ox, oy, k, 13, 3, 6, 13, 12, 13, 11, 21, 18, 11, 12, 11, 13, 3); 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; break;
case IconKind.Folder: case IconKind.Folder:
Poly(g, pen, ox, oy, k, 3, 7, 9, 7, 11, 9, 21, 9, 21, 19, 3, 19, 3, 7); 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; break;
case IconKind.Doc: 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, 5.8f, 3f, 14.6f, 3f, 19f, 7.4f, 19f, 21f, 5.8f, 21f, 5.8f, 3f);
Poly(g, pen, ox, oy, k, 15, 3, 15, 7, 19, 7); Poly(g, pen, ox, oy, k, 14.6f, 3f, 14.6f, 7.4f, 19f, 7.4f);
Line(g, pen, ox, oy, k, 9, 12, 16, 12); Line(g, pen, ox, oy, k, 8.8f, 12.2f, 16f, 12.2f);
Line(g, pen, ox, oy, k, 9, 16, 16, 16); Line(g, pen, ox, oy, k, 8.8f, 16.2f, 16f, 16.2f);
break; break;
case IconKind.ArrowUp: case IconKind.ArrowUp:
Line(g, pen, ox, oy, k, 12, 20, 12, 5); Line(g, pen, ox, oy, k, 12f, 20.4f, 12f, 5.2f);
Poly(g, pen, ox, oy, k, 5, 12, 12, 5, 19, 12); Poly(g, pen, ox, oy, k, 5.8f, 11.4f, 12f, 5.2f, 18.2f, 11.4f);
break; break;
case IconKind.Wrench: case IconKind.Wrench: // 实心开口扳手:环+手柄在 16px 下会读成放大镜,实心剪影才立得住
g.DrawArc(pen, ox + 3 * k, oy + 3 * k, 8 * k, 8 * k, 300f, 250f); var wst = g.Save();
Line(g, pen, ox, oy, k, 10, 14, 19, 21); 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; break;
case IconKind.Trash: case IconKind.Trash:
Poly(g, pen, ox, oy, k, 4, 7, 20, 7); Line(g, pen, ox, oy, k, 4.2f, 8.2f, 19.8f, 8.2f); // 盖
Poly(g, pen, ox, oy, k, 8, 7, 8, 20, 16, 20, 16, 7); Line(g, pen, ox, oy, k, 9.4f, 4.2f, 14.6f, 4.2f); // 提手(与盖留 4 单位空隙)
Line(g, pen, ox, oy, k, 11, 4, 13, 4); Poly(g, pen, ox, oy, k, 6.8f, 8.2f, 6.8f, 20f, 17.2f, 20f, 17.2f, 8.2f); // 桶身
break; break;
case IconKind.Table: case IconKind.Table:
g.DrawRectangle(pen, ox + 3 * k, oy + 5 * k, 18 * k, 14 * k); var tr = new RectangleF(ox + 2.8f * k, oy + 4.6f * k, 18.4f * k, 14.8f * k);
Line(g, pen, ox, oy, k, 3, 10, 21, 10); using (var path = Theme.RoundRect(tr, 2.2f * k)) g.DrawPath(pen, path);
Line(g, pen, ox, oy, k, 10, 10, 10, 19); 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; break;
case IconKind.Gear: case IconKind.Gear: // 环 + 8 根短齿 + 实心轴:齿短一点才不会糊成太阳
g.DrawEllipse(pen, ox + 8 * k, oy + 8 * k, 8 * k, 8 * k); for (int i = 0; i < 8; i++)
g.DrawEllipse(pen, ox + 10.5f * k, oy + 10.5f * k, 3 * k, 3 * k); {
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; break;
case IconKind.Sync: // 拉取服务器数据:带缺口的圆 + 箭头 case IconKind.Fetch: // 从服务器拉取(加载迭代列表):下箭头落进托盘,与「开始导入」的上箭头成对
g.DrawArc(pen, ox + 4 * k, oy + 4 * k, 16 * k, 16 * k, 300f, 250f); // 圆形箭头在 16px 下始终读成「缺口的圈 + 一坨」,这里换直线几何;
Poly(g, pen, ox, oy, k, 19, 4, 20, 9, 15, 8); // 托盘底边取 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; break;
} }
} }
g.SmoothingMode = old; 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) private static void Poly(Graphics g, Pen pen, float ox, float oy, float k, params float[] xy)
{ {
var pts = new PointF[xy.Length / 2]; g.DrawLines(pen, Points(ox, oy, k, xy));
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 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) 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); 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 } internal enum BadgeKind { Pending, Active, Done, Warn }
@@ -329,7 +394,8 @@ namespace TfsExcelImporter
var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f); var r = new RectangleF(0.5f, 0.5f, Width - 1f, Height - 1f);
using (var b = new SolidBrush(fill)) g.FillEllipse(b, r); using (var b = new SolidBrush(fill)) g.FillEllipse(b, r);
g.SmoothingMode = old; g.SmoothingMode = old;
if (Kind == BadgeKind.Done) Icons.Draw(g, IconKind.Check, ClientRectangle, ink); // 勾画在圆内缩 4px(14px 盒子 / 22px 圆),铺满整圆会贴边看着像被裁掉
if (Kind == BadgeKind.Done) Icons.Draw(g, IconKind.Check, Icons.BadgeGlyph(ClientRectangle), ink);
else else
TextRenderer.DrawText(g, Number.ToString(), Theme.CaptionBold, ClientRectangle, ink, TextRenderer.DrawText(g, Number.ToString(), Theme.CaptionBold, ClientRectangle, ink,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding);
@@ -505,7 +571,7 @@ namespace TfsExcelImporter
g.SmoothingMode = SmoothingMode.AntiAlias; g.SmoothingMode = SmoothingMode.AntiAlias;
using (var b = new SolidBrush(badgeFill)) g.FillEllipse(b, badge); using (var b = new SolidBrush(badgeFill)) g.FillEllipse(b, badge);
g.SmoothingMode = old; g.SmoothingMode = old;
if (Badge == BadgeKind.Done) Icons.Draw(g, IconKind.Check, badge, badgeInk); if (Badge == BadgeKind.Done) Icons.Draw(g, IconKind.Check, Icons.BadgeGlyph(badge), badgeInk);
else else
TextRenderer.DrawText(g, StepNumber.ToString(), Theme.CaptionBold, badge, badgeInk, TextRenderer.DrawText(g, StepNumber.ToString(), Theme.CaptionBold, badge, badgeInk,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding);
@@ -575,7 +641,7 @@ namespace TfsExcelImporter
Width = Math.Max(Theme.Px(designMin), pad * 2 + gw + gap + Theme.TextWidth(Text, Font)); 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 int GlyphWidth { get { return Glyph == IconKind.None ? 0 : Theme.Px(16); } }
private Color FaceColor private Color FaceColor
{ {
@@ -773,6 +839,10 @@ namespace TfsExcelImporter
/// <summary>宿主右侧留给自绘箭头的区域,同时也是点击展开下拉的热区。</summary> /// <summary>宿主右侧留给自绘箭头的区域,同时也是点击展开下拉的热区。</summary>
private int ArrowZone { get { return Theme.Px(26); } } private int ArrowZone { get { return Theme.Px(26); } }
/// <summary>内容左缘相对 _box 的缩进。TextBox 就摆在 Px(7),组合框闭合态文字必须共用同一基准,
/// 否则同一排的输入框文字会错开一条竖线。</summary>
private static int TextInsetX { get { return Theme.Px(7); } }
public InputField() public InputField()
{ {
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
@@ -851,19 +921,30 @@ namespace TfsExcelImporter
// 闭合态就是普通输入框:白底黑字。焦点/校验态由宿主描边表达, // 闭合态就是普通输入框:白底黑字。焦点/校验态由宿主描边表达,
// 不响应 Selected 标记——否则一聚焦整块变浅蓝,还会把旁边的箭头衬成白色方块。 // 不响应 Selected 标记——否则一聚焦整块变浅蓝,还会把旁边的箭头衬成白色方块。
using (var b = new SolidBrush(Theme.Card)) e.Graphics.FillRectangle(b, full); 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, 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), textRect, Theme.Ink,
Theme.Ink,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis); | TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
return; return;
} }
using (var b = new SolidBrush(selected ? Theme.PrimarySoft : Theme.Card)) using (var b = new SolidBrush(selected ? Theme.PrimarySoft : Theme.Card))
e.Graphics.FillRectangle(b, full); e.Graphics.FillRectangle(b, full);
// 列表项贴着自己窗口的左沿,要补一段缩进才和输入框里的文字对齐; // 列表项贴着自己窗口的左沿(列表窗口对齐宿主外沿),要补上 _box 的 2px 内缩
// 闭合态的组合框本身就摆在宿主内缩处,原生 bounds 已经带了这个缩进。 // 和文字基准缩进,才和闭合态里的文字严格同一条竖线。
int pad = inBox ? 0 : Theme.Px(10); int pad = TextInsetX + 2;
int right = inBox ? ArrowZone : Theme.Px(6); int right = Theme.Px(6);
var text = new Rectangle(full.X + pad, full.Y, var text = new Rectangle(full.X + pad, full.Y,
Math.Max(0, full.Width - pad - right), full.Height); Math.Max(0, full.Width - pad - right), full.Height);
TextRenderer.DrawText(e.Graphics, cb.GetItemText(cb.Items[e.Index]), cb.Font, text, TextRenderer.DrawText(e.Graphics, cb.GetItemText(cb.Items[e.Index]), cb.Font, text,
@@ -955,6 +1036,9 @@ namespace TfsExcelImporter
private void ToggleComboDropDown() private void ToggleComboDropDown()
{ {
if (!_combo.IsHandleCreated) return; if (!_combo.IsHandleCreated) return;
// 先让组合框拿到焦点:没焦点就 CB_SHOWDROPDOWN,列表展开后随即被焦点变化收掉,
// 表现出来就是「点一下闪一下」。
if (!_combo.Focused) _combo.Focus();
bool dropped = SendMessage(_combo.Handle, CB_GETDROPPEDSTATE, IntPtr.Zero, IntPtr.Zero) != IntPtr.Zero; 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); SendMessage(_combo.Handle, CB_SHOWDROPDOWN, new IntPtr(dropped ? 0 : 1), IntPtr.Zero);
} }
@@ -1040,14 +1124,19 @@ namespace TfsExcelImporter
} }
private ComboListHook _listHook; private ComboListHook _listHook;
private IntPtr _listHwnd = IntPtr.Zero;
private bool _listSkinned;
/// <summary>ComboLBox 随组合框句柄一起创建,尽早挂上钩子,第一次下拉就不跳。</summary> /// <summary>ComboLBox 随组合框句柄一起创建,尽早挂上钩子,第一次下拉就不跳。</summary>
private void HookComboList() private void HookComboList()
{ {
if (_listHook != null || _combo == null || !_combo.IsHandleCreated) return; if (_combo == null || !_combo.IsHandleCreated) return;
var info = new ComboInfo(); var info = new ComboInfo();
info.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(ComboInfo)); info.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(ComboInfo));
if (!GetComboBoxInfo(_combo.Handle, ref info) || info.hwndList == IntPtr.Zero) return; 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 = new ComboListHook(this);
_listHook.AssignHandle(info.hwndList); _listHook.AssignHandle(info.hwndList);
} }
@@ -1076,17 +1165,37 @@ namespace TfsExcelImporter
var info = new ComboInfo(); var info = new ComboInfo();
info.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(ComboInfo)); info.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(ComboInfo));
if (!GetComboBoxInfo(_combo.Handle, ref info) || info.hwndList == IntPtr.Zero) return; if (!GetComboBoxInfo(_combo.Handle, ref info) || info.hwndList == IntPtr.Zero) return;
int style = GetWindowLong(info.hwndList, GWL_STYLE); // 列表窗口重建后边框会长回来,按句柄判断是否需要重新剥一次
if ((style & WS_BORDER) != 0) if (_listHwnd != info.hwndList) { _listHwnd = info.hwndList; _listSkinned = false; }
SetWindowLong(info.hwndList, GWL_STYLE, style & ~WS_BORDER);
int ex = GetWindowLong(info.hwndList, GWL_EXSTYLE); bool skin = false;
if ((ex & WS_EX_CLIENTEDGE) != 0) if (!_listSkinned)
SetWindowLong(info.hwndList, GWL_EXSTYLE, ex & ~WS_EX_CLIENTEDGE); {
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; RECT r;
if (!GetWindowRect(info.hwndList, out r)) return; if (!GetWindowRect(info.hwndList, out r)) return;
var host = RectangleToScreen(ClientRectangle); 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, SetWindowPos(info.hwndList, IntPtr.Zero, host.Left, host.Bottom,
host.Width, r.Bottom - r.Top, SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); host.Width, Math.Max(0, r.Bottom - r.Top),
SWP_NOZORDER | SWP_NOACTIVATE | (skin ? SWP_FRAMECHANGED : 0));
} }
protected override void OnPaint(PaintEventArgs e) protected override void OnPaint(PaintEventArgs e)
+2
View File
@@ -1,2 +1,4 @@
[1101/171941.143:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2) [1101/171941.143:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)
[1101/173900.093:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2) [1101/173900.093:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)
[1101/184906.941:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)
[1101/191310.883:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: 系统找不到指定的文件。 (0x2)