refactor: 移除 CueBanner 类及相关按钮,优化界面布局

- 删除 CueBanner.cs 文件,移除输入框的占位提示功能。
- 更新 MainForm.cs 和 MainFormUi.cs,删除与 Browse 按钮相关的代码,简化用户界面。
- 调整控件布局以提升整体用户体验,确保界面更加简洁。
This commit is contained in:
chenshoulin
2026-11-01 23:15:15 +08:00
parent 8824a81758
commit ffc210f827
4 changed files with 363 additions and 114 deletions
+94 -57
View File
@@ -25,7 +25,6 @@ namespace TfsExcelImporter
private TextBox _txtParentPbi = new TextBox();
private TextBox _txtAreaPath = new TextBox();
private Button _btnLoadIterations = new Button();
private Button _btnBrowse = new Button();
private Button _btnTemplate = new Button();
private CheckBox _chkSkip = new CheckBox();
@@ -67,6 +66,8 @@ namespace TfsExcelImporter
private static int G { get { return Theme.Px(8); } }
private static int LogBodyH { get { return Theme.Px(150); } }
private string _lastReportPath = "";
/// <summary>上一次真正执行 Relayout 的客户区尺寸:尺寸没变就不重排(还原/激活时 WM_SIZE 会重发一次)。</summary>
private Size _laidOutSize;
/// <summary>需要在窗口拉宽时跟着变宽的控件。</summary>
private class Stretch
@@ -114,7 +115,15 @@ namespace TfsExcelImporter
Controls.Add(_cardLog);
Controls.Add(_cardAdv);
Resize += (s, e) => Relayout();
Resize += (s, e) =>
{
// 最小化会收到 WM_SIZE(SIZE_MINIMIZED) 加一个假尺寸(实测 237x39),还原时尺寸又回到原值:
// 这两次重排都白干——最小化那次把 80 多个控件重摆到 237x39 上,还原那次再摆回去,
// 而还原时窗口已经可见,UI 线程却卡在重排里(实测 123ms)不出像素,屏幕上就是一片黑。
if (WindowState == FormWindowState.Minimized) return;
if (ClientSize == _laidOutSize) return;
Relayout();
};
_uiReady = true;
Relayout();
@@ -168,17 +177,18 @@ namespace TfsExcelImporter
private void BuildRibbon()
{
// 设计方案:连接信息栏与步骤卡片同一套白卡视觉,「灰标签 + 深值」分组,不再用淡蓝底
_ribbon = new Surface
{
LineColor = Theme.PrimarySoftBorder,
FaceColor = Theme.PrimarySoft,
LineColor = Theme.Border,
FaceColor = Theme.Card,
Height = Theme.Px(44)
};
AddRibbonGroup("服务器", true);
AddRibbonGroup("项目", true);
AddRibbonGroup("工作项", false);
_chipConn = new StatusChip { BackColor = Theme.PrimarySoft };
_chipConn = new StatusChip { BackColor = Theme.Card };
_ribbon.Controls.Add(_chipConn);
}
@@ -189,9 +199,9 @@ namespace TfsExcelImporter
{
Text = caption,
Font = Theme.Caption,
ForeColor = Theme.Ink2,
ForeColor = Theme.Ink3,
AutoSize = true,
BackColor = Theme.PrimarySoft
BackColor = Theme.Card
};
var val = new Label
{
@@ -199,14 +209,14 @@ namespace TfsExcelImporter
Font = Theme.BodyBold,
ForeColor = Theme.Ink,
AutoSize = true,
BackColor = Theme.PrimarySoft
BackColor = Theme.Card
};
_ribbon.Controls.Add(cap);
_ribbon.Controls.Add(val);
Panel sep = null;
if (withSeparator)
{
sep = new Panel { BackColor = Color.FromArgb(0xC2, 0xD3, 0xEF) };
sep = new Panel { BackColor = Theme.Border };
_ribbon.Controls.Add(sep);
}
_ribItems.Add(new RibGroup { Cap = cap, Val = val, Sep = sep });
@@ -220,15 +230,15 @@ namespace TfsExcelImporter
var cap = group.Cap;
var val = group.Val;
var sep = group.Sep;
cap.SetBounds(x, (_ribbon.Height - cap.PreferredHeight) / 2, cap.PreferredWidth, cap.PreferredHeight);
Theme.Place(cap, x, (_ribbon.Height - cap.PreferredHeight) / 2, cap.PreferredWidth, cap.PreferredHeight);
x += cap.Width + Theme.Px(6);
val.SetBounds(x, (_ribbon.Height - val.PreferredHeight) / 2, val.PreferredWidth, val.PreferredHeight);
Theme.Place(val, x, (_ribbon.Height - val.PreferredHeight) / 2, val.PreferredWidth, val.PreferredHeight);
x += val.Width + Theme.Px(12);
if (sep == null) continue;
sep.SetBounds(x - Theme.Px(8), (_ribbon.Height - Theme.Px(16)) / 2, 1, Theme.Px(16));
Theme.Place(sep, x - Theme.Px(8), (_ribbon.Height - Theme.Px(16)) / 2, 1, Theme.Px(16));
x += Theme.Px(5);
}
_chipConn.SetBounds(_ribbon.Width - _chipConn.Width - Theme.Px(12),
Theme.Place(_chipConn, _ribbon.Width - _chipConn.Width - Theme.Px(12),
(_ribbon.Height - _chipConn.Height) / 2, _chipConn.Width, _chipConn.Height);
}
@@ -243,12 +253,11 @@ namespace TfsExcelImporter
_fUser = NewField(_txtUser, 150, false);
_fPwd = NewField(_txtPwd, 150, true);
// 空态占位提示(cue banner):只在该输入框没填内容时显示
CueBanner.Set(_txtUrl, "如 http://server:8080/tfs/DefaultCollection");
CueBanner.Set(_txtProject, "如 Tools_Dev");
CueBanner.Set(_txtType, "如 Task");
CueBanner.Set(_txtUser, "留空用当前 Windows 身份");
CueBanner.Set(_txtPwd, "域账号密码(同上可选)");
// 空态占位提示(cue banner):只在该输入框没填内容时显示。
// 用户/密码不设提示:这两格只有 150 设计px 宽,长句必被裁断,留空即可用当前 Windows 身份。
_fUrl.SetCue("如 http://server:8080/tfs/DefaultCollection");
_fProject.SetCue("如 Tools_Dev");
_fType.SetCue("如 Task");
_btnTest = NewButton("测试连接", BtnKind.Secondary, IconKind.Bolt, 130);
@@ -316,31 +325,37 @@ namespace TfsExcelImporter
int x = M;
for (int i = 0; i < n; i++)
{
_connRowCaptions[i].SetBounds(x, _connRowY, lblW[i], Theme.FieldH);
Theme.Place(_connRowCaptions[i], x, _connRowY, lblW[i], Theme.FieldH);
x += lblW[i] + lblGap;
_connRowFields[i].SetBounds(x, _connRowY, boxW, Theme.FieldH);
Theme.Place(_connRowFields[i], x, _connRowY, boxW, Theme.FieldH);
x += boxW + gap;
}
_btnTest.SetBounds(x - gap + G, _connRowY, _btnTest.Width, Theme.BtnH);
Theme.Place(_btnTest, x - gap + G, _connRowY, _btnTest.Width, Theme.BtnH);
}
// --- 步骤 2:迭代与数据源 ---
private void BuildCardData()
{
_cardData = MakeCard(2, "选择迭代与数据源", true, Theme.Px(44));
_cardData.SummaryDots = true; // 头部摘要按「圆点 + 短语」分段,四项状态一眼可辨
_cmbIteration.DropDownStyle = ComboBoxStyle.DropDown; // 允许手填完整路径
_fIteration = NewField(_cmbIteration, 500, false);
_fExcel = NewField(_txtExcel, 500, true);
_fParent = NewField(_txtParentPbi, 150, true);
CueBanner.Set(_cmbIteration, "选择或手填完整迭代路径,如 Tools_Dev\\Sprint 1");
CueBanner.Set(_txtExcel, "选择 .xlsx 文件,或点右侧「浏览…」");
CueBanner.Set(_txtParentPbi, "如 219975");
_fIteration.SetCue("选择或手填完整迭代路径,如 Tools_Dev\\Sprint 1");
_fExcel.SetCue("选择 .xlsx 文件,或点右侧文件夹图标浏览");
_fParent.SetCue("如 219975");
_btnLoadIterations = NewButton("加载迭代列表", BtnKind.Secondary, IconKind.Fetch, 130);
_btnBrowse = NewButton("浏览…", BtnKind.Secondary, IconKind.Folder, 92);
_btnTemplate = NewButton("导出 Excel 模板", BtnKind.Secondary, IconKind.Doc, 142);
// 行尾两个按钮宽度取齐(取两者较宽的一个):右缘本来就都由 AnchorRight 贴在 M 上,
// 宽度一致后左缘也落在同一条竖线上,两个按钮才成为一列;同时两行 Track 给右侧留的
// 宽度变成同一个值,上面两个输入框的右边框才对得齐。
int wBtn = Math.Max(_btnLoadIterations.Width, _btnTemplate.Width);
_btnLoadIterations.Width = wBtn;
_btnTemplate.Width = wBtn;
_chkSkip = NewCheck("跳过已存在标题", "");
// 三行字段对齐同一条左缘:标签列宽取最大值
@@ -348,11 +363,12 @@ namespace TfsExcelImporter
int y = _cardData.HeaderH + Theme.S12;
y = Row(_cardData, y, 52, new Lbl("目标迭代", wLbl), _fIteration);
AnchorRight(_btnLoadIterations, _cardData, M, _fIteration.Top);
Track(_fIteration, _cardData, _btnLoadIterations.Width + G + M, 0);
Track(_fIteration, _cardData, wBtn + G + M, 0);
y = Row(_cardData, y, 52, new Lbl("Excel 文件", wLbl), _fExcel);
// 设计方案:行内操作(浏览)并入输入框尾部图标,行尾只留右对齐成一列的操作按钮
_fExcel.AddSuffix(IconKind.Folder, BrowseExcel);
AnchorRight(_btnTemplate, _cardData, M, _fExcel.Top);
AnchorRight(_btnBrowse, _cardData, _btnTemplate.Width + G + M, _fExcel.Top);
Track(_fExcel, _cardData, _btnTemplate.Width + _btnBrowse.Width + G * 2 + M, 0);
Track(_fExcel, _cardData, wBtn + G + M, 0);
y = Row(_cardData, y, 42, new Lbl("父 PBI Id", wLbl), _fParent,
new Hint("Task 默认挂到该 backlog item;Excel 里加 PBI Id 列可逐行覆盖"));
// 勾选项不再贴着标签起头:整体右移到上面三行输入框的左缘(跳过标签列)。
@@ -446,24 +462,24 @@ namespace TfsExcelImporter
{
int badge = Theme.Px(22);
int cy = (_actionBar.Height - badge) / 2;
_badgeRun.SetBounds(M + Theme.Px(2), cy, badge, badge);
Theme.Place(_badgeRun, M + Theme.Px(2), cy, badge, badge);
_badgeRun.Set(4, _runBadge);
_btnPreview.SetBounds(M + Theme.Px(42), (_actionBar.Height - _btnPreview.Height) / 2,
Theme.Place(_btnPreview, M + Theme.Px(42), (_actionBar.Height - _btnPreview.Height) / 2,
_btnPreview.Width, _btnPreview.Height);
_btnImport.SetBounds(_btnPreview.Right + G, (_actionBar.Height - _btnImport.Height) / 2,
Theme.Place(_btnImport, _btnPreview.Right + G, (_actionBar.Height - _btnImport.Height) / 2,
_btnImport.Width, _btnImport.Height);
_chipFail.SetBounds(_actionBar.Width - _chipFail.Width - M - Theme.Px(2), cy, _chipFail.Width, _chipFail.Height);
_chipSkip.SetBounds(_chipFail.Left - G - _chipSkip.Width, cy, _chipSkip.Width, _chipSkip.Height);
_chipOk.SetBounds(_chipSkip.Left - G - _chipOk.Width, cy, _chipOk.Width, _chipOk.Height);
Theme.Place(_chipFail, _actionBar.Width - _chipFail.Width - M - Theme.Px(2), cy, _chipFail.Width, _chipFail.Height);
Theme.Place(_chipSkip, _chipFail.Left - G - _chipSkip.Width, cy, _chipSkip.Width, _chipSkip.Height);
Theme.Place(_chipOk, _chipSkip.Left - G - _chipOk.Width, cy, _chipOk.Width, _chipOk.Height);
// 状态文字按最长常规文案实测给宽,且只给一行的高度:
// 宽度够了不折行,万一更长的文案也只省略尾巴,不会把计数拆到第二行。
int statW = StatusLabelWidth();
int statH = Theme.Caption.Height + Theme.Px(4);
_lblStatus.SetBounds(_chipOk.Left - statW - G, (_actionBar.Height - statH) / 2, statW, statH);
Theme.Place(_lblStatus, _chipOk.Left - statW - G, (_actionBar.Height - statH) / 2, statW, statH);
int left = _btnImport.Right + Theme.S16;
int barW = Math.Max(Theme.Px(40), _lblStatus.Left - G - left);
_pbar.SetBounds(left, (_actionBar.Height - _pbar.Height) / 2, barW, _pbar.Height);
Theme.Place(_pbar, left, (_actionBar.Height - _pbar.Height) / 2, barW, _pbar.Height);
}
/// <summary>
@@ -520,16 +536,16 @@ namespace TfsExcelImporter
int x = _cardLog.Width - Theme.Px(12) - Theme.Px(30); // 让开折叠箭头
// 头部所有元素共用一条中线:按钮和勾选框都按 cy - 高度/2 摆,文字才齐平
int cy = _cardLog.HeaderH / 2;
_btnOpenReport.SetBounds(x - _btnOpenReport.Width, cy - _btnOpenReport.Height / 2,
Theme.Place(_btnOpenReport, x - _btnOpenReport.Width, cy - _btnOpenReport.Height / 2,
_btnOpenReport.Width, _btnOpenReport.Height);
_btnClearLog.SetBounds(_btnOpenReport.Left - G - _btnClearLog.Width, cy - _btnClearLog.Height / 2,
Theme.Place(_btnClearLog, _btnOpenReport.Left - G - _btnClearLog.Width, cy - _btnClearLog.Height / 2,
_btnClearLog.Width, _btnClearLog.Height);
// net48 的 CheckBox 没有 PreferredWidth,只能自己量文字宽度(外加勾选框);
// 高度取原生 PreferredHeight,整体压在中线上
var pref = _chkAutoScroll.GetPreferredSize(Size.Empty);
int chkH = Math.Max(Theme.Px(18), pref.Height);
int chkW = Math.Max(Theme.TextWidth(_chkAutoScroll.Text, Theme.Caption) + Theme.Px(22), pref.Width);
_chkAutoScroll.SetBounds(_btnClearLog.Left - chkW - G, cy - chkH / 2, chkW, chkH);
Theme.Place(_chkAutoScroll, _btnClearLog.Left - chkW - G, cy - chkH / 2, chkW, chkH);
});
}
@@ -539,7 +555,7 @@ namespace TfsExcelImporter
_cardAdv = MakeCard(0, "高级工具", true, Theme.Px(36));
_cardAdv.ShowBadge = false;
_fArea = NewField(_txtAreaPath, 420, true);
CueBanner.Set(_txtAreaPath, "如 Tools_Dev\\导入工具,留空则沿用工作项现有 Area");
_fArea.SetCue("如 Tools_Dev\\导入工具,留空则沿用工作项现有 Area");
_btnFixArea = NewButton("按迭代修 Area", BtnKind.Secondary, IconKind.Wrench, 158);
_btnLinkParent = NewButton("补挂父项", BtnKind.Secondary, IconKind.Tree, 120);
@@ -753,22 +769,22 @@ namespace TfsExcelImporter
{
int cw = Math.Max(Theme.Px(320), ClientSize.Width - M * 2);
_ribbon.SetBounds(M, M, cw, _ribbon.Height);
Theme.Place(_ribbon, M, M, cw, _ribbon.Height);
int y = _ribbon.Bottom + G;
foreach (var card in new[] { _cardConn, _cardData })
{
card.SetBounds(M, y, cw, card.Height);
Theme.Place(card, M, y, cw, card.Height);
y = card.Bottom + G;
}
// 从底部往上排
int by = ClientSize.Height - M;
_cardAdv.SetBounds(M, by - _cardAdv.Height, cw, _cardAdv.Height);
Theme.Place(_cardAdv, M, by - _cardAdv.Height, cw, _cardAdv.Height);
by = _cardAdv.Top - G;
_cardLog.SetBounds(M, by - _cardLog.Height, cw, _cardLog.Height);
Theme.Place(_cardLog, M, by - _cardLog.Height, cw, _cardLog.Height);
by = _cardLog.Top - G;
_actionBar.SetBounds(M, by - _actionBar.Height, cw, _actionBar.Height);
Theme.Place(_actionBar, M, by - _actionBar.Height, cw, _actionBar.Height);
by = _actionBar.Top - G;
// 预览卡吃剩余高度;空间不足时先让日志收成一行
@@ -777,14 +793,14 @@ namespace TfsExcelImporter
_cardLog.Expanded = false;
_cardLog.ApplyHeight();
by = ClientSize.Height - M;
_cardAdv.SetBounds(M, by - _cardAdv.Height, cw, _cardAdv.Height);
Theme.Place(_cardAdv, M, by - _cardAdv.Height, cw, _cardAdv.Height);
by = _cardAdv.Top - G;
_cardLog.SetBounds(M, by - _cardLog.Height, cw, _cardLog.Height);
Theme.Place(_cardLog, M, by - _cardLog.Height, cw, _cardLog.Height);
by = _cardLog.Top - G;
_actionBar.SetBounds(M, by - _actionBar.Height, cw, _actionBar.Height);
Theme.Place(_actionBar, M, by - _actionBar.Height, cw, _actionBar.Height);
by = _actionBar.Top - G;
}
_cardPreview.SetBounds(M, y, cw, Math.Max(Theme.Px(120), by - G - y));
Theme.Place(_cardPreview, M, y, cw, Math.Max(Theme.Px(120), by - G - y));
_cardPreview.BodyH = _cardPreview.Height - _cardPreview.HeaderH;
foreach (var st in _stretches)
@@ -792,18 +808,34 @@ namespace TfsExcelImporter
if (st.C == null || st.Owner == null) continue;
int w = Math.Max(Theme.Px(60), st.Owner.Width - st.X - st.Right);
int h = st.FillHeight ? Math.Max(Theme.Px(20), st.Owner.Height - st.Y - st.Bottom) : st.C.Height;
st.C.SetBounds(st.X, st.Y, w, h);
Theme.Place(st.C, st.X, st.Y, w, h);
}
foreach (var ra in _rightAnchors)
{
if (ra.C == null || ra.Owner == null) continue;
ra.C.SetBounds(ra.Owner.Width - ra.FromRight - ra.C.Width, ra.Y, ra.C.Width, ra.C.Height);
Theme.Place(ra.C, ra.Owner.Width - ra.FromRight - ra.C.Width, ra.Y, ra.C.Width, ra.C.Height);
}
LayoutRibbon();
foreach (var act in _layoutExtras) act();
_cardConn.Invalidate();
_cardData.Invalidate();
_laidOutSize = ClientSize;
}
/// <summary>
/// 自绘界面平时不擦背景(AllPaintingInWmPaint + 双缓冲),窗口被重新暴露时那块像素是系统给的黑色。
/// 还原/切前台时完整首帧要一百多毫秒,这段时间就是用户看到的「黑屏一闪」;
/// 这里在 WM_ERASEBKGND 上用 GDI 直接把客户区铺成面色,黑底一秒都不露,完整首帧随后补上细节。
/// </summary>
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);
}
// ------------------------------------------------------------- 状态呈现
@@ -962,7 +994,7 @@ namespace TfsExcelImporter
_btnLoadIterations.Enabled = !busy;
_btnFixArea.Enabled = !busy;
_btnLinkParent.Enabled = !busy;
_btnBrowse.Enabled = !busy;
_fExcel.SetSuffixEnabled(!busy); // 「浏览」已并入输入框尾部,随忙等一起禁用
_btnTemplate.Enabled = !busy;
}
}
@@ -981,16 +1013,21 @@ namespace TfsExcelImporter
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
int icon = Theme.Px(40);
int well = Theme.Px(48); // 圆形浅灰图标底:裸图标太轻,兜一层「底座」才有空状态的分量
int icon = Theme.Px(22);
int textH = Theme.Px(18);
int blockH = icon + Theme.Px(10) + textH;
int blockH = well + Theme.Px(12) + textH;
int top = Math.Max(Theme.Px(4), (Height - blockH) / 2);
int wellX = (Width - well) / 2;
var old = g.SmoothingMode;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Icons.Draw(g, IconKind.Table, new Rectangle((Width - icon) / 2, top, icon, icon), Theme.Ink3);
using (var b = new SolidBrush(Theme.Disabled))
g.FillEllipse(b, wellX, top, well, well);
Icons.Draw(g, IconKind.Table,
new Rectangle(wellX + (well - icon) / 2, top + (well - icon) / 2, icon, icon), Theme.Ink3);
g.SmoothingMode = old;
TextRenderer.DrawText(g, Text, Theme.Body,
new Rectangle(Theme.Px(16), top + icon + Theme.Px(10), Width - Theme.Px(32), textH),
new Rectangle(Theme.Px(16), top + well + Theme.Px(12), Width - Theme.Px(32), textH),
Theme.Ink3,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);