refactor: 移除 CueBanner 类及相关按钮,优化界面布局
- 删除 CueBanner.cs 文件,移除输入框的占位提示功能。 - 更新 MainForm.cs 和 MainFormUi.cs,删除与 Browse 按钮相关的代码,简化用户界面。 - 调整控件布局以提升整体用户体验,确保界面更加简洁。
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace TfsExcelImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 输入框空态占位文字(cue banner)。net48 的 TextBox 没有 PlaceholderText,
|
||||
/// 用原生 EM_SETCUEBANNER 实现;ComboBox(DropDown)要发给它内嵌的 Edit 子窗口,
|
||||
/// 直接发给组合框本体无效。文字只在输入框为空时显示,与 Web 表单 placeholder 手感一致。
|
||||
/// </summary>
|
||||
internal static class CueBanner
|
||||
{
|
||||
private const int EM_SETCUEBANNER = 0x1501;
|
||||
private const uint GW_CHILD = 5;
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, string lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetWindow(IntPtr hWnd, uint cmd);
|
||||
|
||||
/// <summary>设置占位文字。控件句柄可能尚未创建,挂 HandleCreated 兜底;
|
||||
/// wParam=1 让占位文字在聚焦时也显示(有真实输入后自然消失)。</summary>
|
||||
public static void Set(Control c, string text)
|
||||
{
|
||||
if (c == null || string.IsNullOrEmpty(text)) return;
|
||||
Action apply = () => Send(c, text);
|
||||
if (c.IsHandleCreated) apply();
|
||||
c.HandleCreated += (s, e) => apply();
|
||||
}
|
||||
|
||||
private static void Send(Control c, string text)
|
||||
{
|
||||
IntPtr target = c.Handle;
|
||||
if (c is ComboBox)
|
||||
{
|
||||
// DropDown 组合框的内嵌 Edit 子窗口:cue banner 只认它
|
||||
target = GetWindow(c.Handle, GW_CHILD);
|
||||
if (target == IntPtr.Zero) return;
|
||||
}
|
||||
SendMessage(target, EM_SETCUEBANNER, (IntPtr)1, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ namespace TfsExcelImporter
|
||||
{
|
||||
_btnTest.Click += async (s, e) => await TestConnectionAsync();
|
||||
_btnLoadIterations.Click += async (s, e) => await LoadIterationsAsync();
|
||||
_btnBrowse.Click += (s, e) => BrowseExcel();
|
||||
_btnTemplate.Click += (s, e) => ExportTemplate();
|
||||
_btnPreview.Click += async (s, e) => await PreviewDataAsync();
|
||||
_btnImport.Click += async (s, e) => await ImportAsync();
|
||||
|
||||
@@ -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);
|
||||
|
||||
+266
-8
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace TfsExcelImporter
|
||||
@@ -149,10 +151,64 @@ namespace TfsExcelImporter
|
||||
return Card;
|
||||
}
|
||||
|
||||
public static int TextWidth(string text, Font font) {
|
||||
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;
|
||||
return TextRenderer.MeasureText(text, font, Size.Empty,
|
||||
// 排版会反复量同一批文案(拖拽缩放每帧都要全量量一遍),而 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)
|
||||
@@ -393,6 +449,8 @@ namespace TfsExcelImporter
|
||||
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);
|
||||
@@ -423,6 +481,16 @@ namespace TfsExcelImporter
|
||||
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,
|
||||
@@ -446,6 +514,8 @@ namespace TfsExcelImporter
|
||||
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;
|
||||
@@ -461,6 +531,17 @@ namespace TfsExcelImporter
|
||||
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); } }
|
||||
@@ -570,6 +651,10 @@ namespace TfsExcelImporter
|
||||
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
|
||||
@@ -588,8 +673,12 @@ namespace TfsExcelImporter
|
||||
| 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,
|
||||
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);
|
||||
@@ -605,6 +694,40 @@ namespace TfsExcelImporter
|
||||
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 }
|
||||
@@ -831,14 +954,25 @@ namespace TfsExcelImporter
|
||||
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); } }
|
||||
@@ -896,16 +1030,56 @@ namespace TfsExcelImporter
|
||||
_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();
|
||||
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)
|
||||
@@ -953,14 +1127,19 @@ namespace TfsExcelImporter
|
||||
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
|
||||
}
|
||||
|
||||
private void LayoutInner()
|
||||
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) continue;
|
||||
if (c is Chevron || c is SuffixIcon || c is CueLabel) continue;
|
||||
var tb = c as TextBox;
|
||||
var cb = c as ComboBox;
|
||||
if (cb != null)
|
||||
@@ -974,7 +1153,13 @@ namespace TfsExcelImporter
|
||||
}
|
||||
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);
|
||||
// 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)
|
||||
{
|
||||
@@ -982,6 +1167,21 @@ namespace TfsExcelImporter
|
||||
_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();
|
||||
}
|
||||
|
||||
@@ -1018,6 +1218,64 @@ namespace TfsExcelImporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
Reference in New Issue
Block a user