feat: 添加系统 DPI 感知支持与界面优化

- 新增 app.manifest 文件以实现系统 DPI 感知,确保界面在不同缩放设置下正常显示。
- 更新 MainForm.cs 和 Theme.cs,调整控件布局和样式以适应 DPI 变化。
- 修改 README.md,增加关于 DPI 感知的说明。
- 新增 MainFormUi.cs,负责界面布局与状态呈现,优化用户体验。
- 新增 debug.log 文件以记录错误信息,便于调试和问题追踪。
This commit is contained in:
chenshoulin
2026-11-01 18:47:41 +08:00
parent c2d121d185
commit dbe8ff2c08
7 changed files with 2361 additions and 282 deletions
+12 -8
View File
@@ -36,7 +36,10 @@ TFSImport/
│ └── TFS导入模板.xlsx # 默认模板:csproj 随构建拷到 exe 目录,「导出Excel模板」原样复制它
└── TfsExcelImporter/ # C# WinForms 桌面版(主用)
├── TfsExcelImporter.csproj # net48 + ClosedXML + TFS ExtendedClient 14.*
├── MainForm.cs # 界面与导入/修 Area 流程
├── MainForm.cs # 导入 / 修 Area / 补挂父项流程
├── MainFormUi.cs # 界面外壳:步骤卡片布局、手风琴折叠、状态呈现
├── Theme.cs # 设计 token(颜色/字号/间距/缩放)与自绘控件
├── app.manifest # System DPI 感知,像素尺寸按缩放换算
├── TfsClient.cs # TFS Client OM 封装
├── ExcelService.cs # 读 Excel、导出默认模板、时间/数值归一
├── Config.cs # exe 同目录 config.json 读写
@@ -55,14 +58,15 @@ TfsExcelImporter\bin\Release\net48\TfsExcelImporter.exe
「导出Excel模板」复制的是 exe 同目录的 `TFS导入模板.xlsx`(构建时自动拷过去);单独分发 exe 时要带上这个文件,否则该按钮会报「未找到默认模板文件」。
操作流程:
界面按 4 步排:顶部状态条常驻显示服务器/项目/类型与连接状态,四张卡片一次只展开一张(展开一张会把其它收成一行摘要),预览表格永远占满剩余高度。
1. 填「TFS 连接」区:URL、项目、工作项类型、认证方式与域账号;内网保持「使用系统代理」**不勾选**。
2. 点「加载迭代列表」,选择目标迭代。
3. 点「导出Excel模板」拿到默认模板,或「浏览...」选择已有 `.xlsx`;点「预览数据」核对。
4. 需要挂父项时填「父 PBI Id」(默认全部挂同一个;Excel 里加 `PBI Id` 列可逐行指定不同父项)。
5. 点「开始导入 TFS」,日志区逐行输出 `OK/SKIP/FAIL` 与工作项 ID。
6. 历史数据 Area 不对时用「按迭代修Area」批量纠正。
1. **步骤 1 连接**:填 URL、项目、工作项类型、认证方式与域账号,点「测试连接」——右上状态灯变绿即连通;内网保持「使用系统代理」**不勾选**。
2. **步骤 2 选数据**:点「加载迭代列表」选目标迭代(也可手填完整路径);点「导出 Excel 模板」拿默认模板,或「浏览...」选已有 `.xlsx`。需要挂父项时填「父 PBI Id」(默认全部挂同一个;Excel 里加 `PBI Id` 列可逐行指定不同父项)。
3. **步骤 3 预览核对**:选完文件自动读表,表格里逐行核对;缺迭代/文件路径无效时红字直接标在对应输入框下方,不再弹框。
4. **步骤 4 导入**:底部操作条点「开始导入 TFS」,进度条 + `n/m` + 成功/跳过/失败三个计数实时更新,日志抽屉逐行输出 `OK/SKIP/FAIL` 与工作项 ID;跑完点「打开报告 CSV」看逐行结果。
5. 历史数据 Area 不对、或漏挂父项时,展开底部「高级工具」用「按迭代修 Area」「补挂父项」批量纠正;Area 路径也在这一栏里填(以前只能手改 config.json)。
桌面版按 System DPI 感知实现,界面尺寸随 Windows 显示缩放换算;**改了显示缩放比例要重启程序**才生效。
### Python 版
+216 -282
View File
@@ -9,239 +9,68 @@ using System.Windows.Forms;
namespace TfsExcelImporter
{
public class MainForm : Form
/// <summary>
/// 导入流程与 TFS/Excel 调用。界面摆放与状态呈现见 MainFormUi.cs,自绘控件见 Theme.cs。
/// </summary>
public partial class MainForm : Form
{
// ------------------------------------------------------------- 控件
private readonly TextBox _txtUrl = new TextBox();
private readonly TextBox _txtProject = new TextBox();
private readonly TextBox _txtType = new TextBox();
private readonly ComboBox _cmbAuth = new ComboBox();
private readonly TextBox _txtUser = new TextBox();
private readonly TextBox _txtPwd = new TextBox { UseSystemPasswordChar = true };
private readonly TextBox _txtDomain = new TextBox();
private readonly CheckBox _chkProxy = new CheckBox();
private readonly ComboBox _cmbIteration = new ComboBox();
private readonly Button _btnLoadIterations = new Button();
private readonly TextBox _txtExcel = new TextBox();
private readonly Button _btnBrowse = new Button();
private readonly Button _btnTemplate = new Button();
private readonly CheckBox _chkSkip = new CheckBox();
private readonly CheckBox _chkBypass = new CheckBox();
private readonly TextBox _txtParentPbi = new TextBox();
private readonly DataGridView _grid = new DataGridView();
private readonly Button _btnPreview = new Button();
private readonly Button _btnImport = new Button();
private readonly Button _btnFixArea = new Button();
private readonly Button _btnLinkParent = new Button();
private readonly TextBox _txtLog = new TextBox();
private readonly Label _lblStatus = new Label();
private AppConfig _cfg;
private List<WorkItemRow> _rows = new List<WorkItemRow>();
public MainForm()
{
// System DPI 感知(见 app.manifest):先定缩放系数,再按它摆控件
using (var g = Graphics.FromHwnd(IntPtr.Zero))
Theme.Scale = g.DpiX / 96f;
_cfg = AppConfig.Load();
BuildUi();
LoadConfigToUi();
WireEvents();
RefreshRibbon();
RefreshSummaries();
SetConnection("连接未验证", ChipKind.Neutral);
UpdateEmptyState();
}
// ------------------------------------------------------------- UI 构建
private void BuildUi()
private void WireEvents()
{
Text = "TFS 2013 工作项 Excel 导入工具";
ClientSize = new Size(1060, 720);
MinimumSize = new Size(960, 640);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Microsoft YaHei UI", 9F);
int y = 12, left = 12, lblW = 84;
// --- 服务器配置区 ---
var grpServer = new GroupBox
{
Text = "TFS 连接",
Location = new Point(left, y),
Size = new Size(1036, 120),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
};
Controls.Add(grpServer);
AddLabel(grpServer, "服务器URL:", 12, 24, lblW);
_txtUrl.Location = new Point(100, 22); _txtUrl.Size = new Size(460, 27);
_txtUrl.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
grpServer.Controls.Add(_txtUrl);
_chkProxy.Text = "使用系统代理";
_chkProxy.Location = new Point(564, 24); _chkProxy.Size = new Size(110, 24);
_chkProxy.Anchor = AnchorStyles.Top | AnchorStyles.Right;
_chkProxy.Checked = false; // 内网 TFS 默认直连
grpServer.Controls.Add(_chkProxy);
AddLabel(grpServer, "团队项目:", 680, 24, lblW);
_txtProject.Location = new Point(768, 22); _txtProject.Size = new Size(150, 27);
_txtProject.Anchor = AnchorStyles.Top | AnchorStyles.Right;
grpServer.Controls.Add(_txtProject);
AddLabel(grpServer, "工作项类型:", 12, 58, lblW);
_txtType.Location = new Point(100, 56); _txtType.Size = new Size(220, 27);
grpServer.Controls.Add(_txtType);
AddLabel(grpServer, "认证方式:", 340, 58, lblW);
_cmbAuth.Location = new Point(428, 56); _cmbAuth.Size = new Size(110, 27);
_cmbAuth.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbAuth.Items.AddRange(new object[] { "ntlm", "basic", "default" });
_cmbAuth.SelectedIndexChanged += (s, e) => UpdateAuthUi();
grpServer.Controls.Add(_cmbAuth);
AddLabel(grpServer, "用户:", 556, 58, 44);
_txtUser.Location = new Point(604, 56); _txtUser.Size = new Size(110, 27);
grpServer.Controls.Add(_txtUser);
AddLabel(grpServer, "密码:", 726, 58, 44);
_txtPwd.Location = new Point(774, 56); _txtPwd.Size = new Size(110, 27);
grpServer.Controls.Add(_txtPwd);
AddLabel(grpServer, "域:", 896, 58, 28);
_txtDomain.Location = new Point(928, 56); _txtDomain.Size = new Size(94, 27);
_txtDomain.Anchor = AnchorStyles.Top | AnchorStyles.Right;
grpServer.Controls.Add(_txtDomain);
y += 128;
// --- 导入选区 ---
var grpImport = new GroupBox
{
Text = "导入选区",
Location = new Point(left, y),
Size = new Size(1036, 120),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
};
Controls.Add(grpImport);
AddLabel(grpImport, "目标Iteration:", 12, 26, 96);
_cmbIteration.Location = new Point(112, 23); _cmbIteration.Size = new Size(560, 27);
_cmbIteration.DropDownStyle = ComboBoxStyle.DropDown; // 允许手填
_cmbIteration.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
grpImport.Controls.Add(_cmbIteration);
_btnLoadIterations.Text = "加载迭代列表";
_btnLoadIterations.Location = new Point(682, 22); _btnLoadIterations.Size = new Size(110, 29);
_btnLoadIterations.Anchor = AnchorStyles.Top | AnchorStyles.Right;
_cmbAuth.SelectedIndexChanged += (s, e) => { UpdateAuthUi(); OnFieldChanged(); };
_btnTest.Click += async (s, e) => await TestConnectionAsync();
_btnLoadIterations.Click += async (s, e) => await LoadIterationsAsync();
grpImport.Controls.Add(_btnLoadIterations);
AddLabel(grpImport, "Excel 文件:", 12, 62, 96);
_txtExcel.Location = new Point(112, 59); _txtExcel.Size = new Size(560, 27);
_txtExcel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
grpImport.Controls.Add(_txtExcel);
_btnBrowse.Text = "浏览...";
_btnBrowse.Location = new Point(682, 58); _btnBrowse.Size = new Size(70, 29);
_btnBrowse.Anchor = AnchorStyles.Top | AnchorStyles.Right;
_btnBrowse.Click += (s, e) => BrowseExcel();
grpImport.Controls.Add(_btnBrowse);
_btnTemplate.Text = "导出Excel模板";
_btnTemplate.Location = new Point(760, 58); _btnTemplate.Size = new Size(110, 29);
_btnTemplate.Anchor = AnchorStyles.Top | AnchorStyles.Right;
_btnTemplate.Click += (s, e) => ExportTemplate();
grpImport.Controls.Add(_btnTemplate);
_chkSkip.Text = "跳过已存在标题";
_chkSkip.Location = new Point(886, 26); _chkSkip.Size = new Size(130, 24);
_chkSkip.Anchor = AnchorStyles.Top | AnchorStyles.Right;
grpImport.Controls.Add(_chkSkip);
_chkBypass.Text = "bypassRules(绕过字段规则)";
_chkBypass.Location = new Point(886, 60); _chkBypass.Size = new Size(150, 24);
_chkBypass.Anchor = AnchorStyles.Top | AnchorStyles.Right;
grpImport.Controls.Add(_chkBypass);
// 父 PBI:全部 Task 默认挂接的 Product Backlog Item(Excel 每行 PBI Id 列可覆盖)
AddLabel(grpImport, "父PBI Id:", 12, 92, 96);
_txtParentPbi.Location = new Point(112, 89); _txtParentPbi.Size = new Size(200, 27);
grpImport.Controls.Add(_txtParentPbi);
var lblPbiHint = new Label
{
Text = "Task 将挂接为该 Product Backlog Item 的子项;Excel 加 PBI Id 列可逐行指定不同父项",
Location = new Point(320, 92), Size = new Size(560, 22),
TextAlign = ContentAlignment.MiddleLeft,
ForeColor = SystemColors.GrayText
};
grpImport.Controls.Add(lblPbiHint);
y += 128;
// --- 预览表格 ---
_grid.Location = new Point(left, y);
_grid.Size = new Size(1036, 300);
_grid.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
_grid.ReadOnly = true;
_grid.AllowUserToAddRows = false;
_grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells;
_grid.RowHeadersVisible = false;
_grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
Controls.Add(_grid);
y += 308;
// --- 操作按钮 ---
_btnPreview.Text = "预览数据";
_btnPreview.Location = new Point(left, y); _btnPreview.Size = new Size(110, 32);
_btnPreview.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
_btnPreview.Click += (s, e) => PreviewData();
Controls.Add(_btnPreview);
_btnImport.Text = "开始导入 TFS";
_btnImport.Location = new Point(left + 122, y); _btnImport.Size = new Size(130, 32);
_btnImport.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
_btnImport.BackColor = Color.FromArgb(0, 120, 215);
_btnImport.ForeColor = Color.White;
_btnImport.Click += async (s, e) => await ImportAsync();
Controls.Add(_btnImport);
_btnFixArea.Text = "按迭代修Area";
_btnFixArea.Location = new Point(left + 264, y); _btnFixArea.Size = new Size(130, 32);
_btnFixArea.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
_btnFixArea.Click += async (s, e) => await FixAreaAsync();
Controls.Add(_btnFixArea);
_btnLinkParent.Text = "补挂父项";
_btnLinkParent.Location = new Point(left + 406, y); _btnLinkParent.Size = new Size(130, 32);
_btnLinkParent.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
_btnLinkParent.Click += async (s, e) => await LinkParentAsync();
Controls.Add(_btnLinkParent);
_lblStatus.Text = "就绪";
_lblStatus.Location = new Point(left + 548, y + 6); _lblStatus.Size = new Size(400, 22);
_lblStatus.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
Controls.Add(_lblStatus);
y += 40;
foreach (var tb in new[] { _txtUrl, _txtProject, _txtType, _txtUser, _txtPwd, _txtDomain,
_txtExcel, _txtParentPbi, _txtAreaPath })
tb.TextChanged += (s, e) => OnFieldChanged();
_cmbIteration.TextChanged += (s, e) => OnFieldChanged();
_chkSkip.CheckedChanged += (s, e) => OnFieldChanged();
_chkBypass.CheckedChanged += (s, e) => OnFieldChanged();
_chkProxy.CheckedChanged += (s, e) => OnFieldChanged();
// --- 日志 ---
_txtLog.Location = new Point(left, y);
_txtLog.Size = new Size(1036, 120);
_txtLog.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
_txtLog.Multiline = true;
_txtLog.ScrollBars = ScrollBars.Vertical;
_txtLog.ReadOnly = true;
Controls.Add(_txtLog);
FormClosing += (s, e) => { try { SaveUiToConfig(); } catch { } };
}
private static void AddLabel(Control parent, string text, int x, int y, int w)
private void OnFieldChanged()
{
var lbl = new Label { Text = text, Location = new Point(x, y), Size = new Size(w, 22), TextAlign = ContentAlignment.MiddleLeft };
parent.Controls.Add(lbl);
RefreshRibbon();
RefreshSummaries();
}
// ------------------------------------------------------------- 认证区联动
private void UpdateAuthUi()
{
bool needCreds = _cmbAuth.SelectedItem != null && _cmbAuth.SelectedItem.ToString() != "default";
_txtUser.Enabled = needCreds;
_txtPwd.Enabled = needCreds;
_txtDomain.Enabled = needCreds;
_fUser.Invalid = false;
_fPwd.Invalid = false;
_fDomain.Invalid = false;
}
// ------------------------------------------------------------- 配置互转
@@ -258,6 +87,7 @@ namespace TfsExcelImporter
_chkProxy.Checked = _cfg.UseSystemProxy;
_cmbIteration.Text = _cfg.IterationPath;
_txtExcel.Text = _cfg.LastExcelPath;
_txtAreaPath.Text = _cfg.AreaPath;
_chkSkip.Checked = _cfg.SkipExistingTitles;
_chkBypass.Checked = _cfg.BypassRules;
_txtParentPbi.Text = _cfg.ParentPbiId;
@@ -276,6 +106,7 @@ namespace TfsExcelImporter
_cfg.UseSystemProxy = _chkProxy.Checked;
_cfg.IterationPath = _cmbIteration.Text.Trim();
_cfg.LastExcelPath = _txtExcel.Text.Trim();
_cfg.AreaPath = _txtAreaPath.Text.Trim();
_cfg.SkipExistingTitles = _chkSkip.Checked;
_cfg.BypassRules = _chkBypass.Checked;
_cfg.ParentPbiId = _txtParentPbi.Text.Trim();
@@ -285,57 +116,132 @@ namespace TfsExcelImporter
// ------------------------------------------------------------- 日志
private void Log(string msg)
{
if (InvokeRequired) { BeginInvoke(new Action(() => Log(msg))); return; }
_txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");
Append(msg, LogView.Classify(msg));
}
// ------------------------------------------------------------- 迭代加载
private async Task LoadIterationsAsync()
private static string FirstLine(string text)
{
if (string.IsNullOrEmpty(text)) return "";
var i = text.IndexOf('\n');
return i < 0 ? text : text.Substring(0, i).Trim();
}
// ------------------------------------------------------------- 连接测试
private async Task TestConnectionAsync()
{
SaveUiToConfig();
_btnLoadIterations.Enabled = false;
if (string.IsNullOrWhiteSpace(_cfg.TfsUrl))
{
SetConnection("URL 未填", ChipKind.Danger);
SetBadgeOf(_cardConn, BadgeKind.Warn);
Append("请先填写服务器 URL", LogLevel.Warn);
_txtUrl.Focus();
return;
}
SetConnection("连接中…", ChipKind.Info);
SetBadgeOf(_cardConn, BadgeKind.Active);
_btnTest.Enabled = false;
try
{
using (var client = new TfsClient(_cfg))
{
Log("测试连接...");
Log(await client.TestConnectionAsync());
// 提前探测导入所需字段接口,避免导入时才发现路由缺失
try
{
var fm = await client.GetFieldMapAsync();
Log($"字段接口可用(字段 {fm.Count} 个)");
Log($"工作项字段元数据可用({fm.Count} 个字段)");
}
catch (Exception ex)
{
Log("⚠ 字段接口(_apis/wit/fields)不可用,导入可能受影响: " + ex.Message.Split('\n')[0]);
}
var paths = await client.GetIterationPathsAsync(_cfg.Project, Log);
_cmbIteration.Items.Clear();
foreach (var p in paths) _cmbIteration.Items.Add(p);
if (_cmbIteration.Items.Count > 0 && string.IsNullOrWhiteSpace(_cmbIteration.Text))
_cmbIteration.SelectedIndex = 0;
// 优先选中配置中的迭代
for (int i = 0; i < _cmbIteration.Items.Count; i++)
{
if (string.Equals(_cmbIteration.Items[i].ToString(), _cfg.IterationPath,
StringComparison.OrdinalIgnoreCase))
{
_cmbIteration.SelectedIndex = i;
break;
Log("警告: 字段元数据获取失败,导入可能受影响: " + FirstLine(ex.Message));
}
}
Log($"已加载 {_cmbIteration.Items.Count} 个迭代路径");
}
SetConnection("已连接", ChipKind.Ok);
SetBadgeOf(_cardConn, BadgeKind.Done);
OpenDetail(_cardData);
}
catch (Exception ex)
{
Log("加载迭代失败: " + ex.Message);
MessageBox.Show(this, "加载迭代失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
SetConnection("连接失败", ChipKind.Danger);
SetBadgeOf(_cardConn, BadgeKind.Warn);
Log("连接失败: " + FirstLine(ex.Message));
Log("排查: 内网请确认「使用系统代理」保持关闭;DNS 失败多为 VPN 或未连内网。");
_cardConn.Expanded = true;
_cardConn.ApplyHeight();
Relayout();
}
finally
{
_btnLoadIterations.Enabled = true;
_btnTest.Enabled = true;
}
}
// ------------------------------------------------------------- 迭代加载
private async Task LoadIterationsAsync()
{
SaveUiToConfig();
if (string.IsNullOrWhiteSpace(_cfg.TfsUrl))
{
Append("请先填写服务器 URL", LogLevel.Warn);
OpenDetail(_cardConn);
return;
}
SetBusy(true);
SetConnection("读取迭代中…", ChipKind.Info);
try
{
using (var client = new TfsClient(_cfg))
{
Log("测试连接...");
Log(await client.TestConnectionAsync());
// 提前探测导入所需字段元数据,避免导入时才发现取不到
try
{
var fm = await client.GetFieldMapAsync();
Log($"工作项字段元数据可用({fm.Count} 个字段)");
}
catch (Exception ex)
{
Log("警告: 字段元数据获取失败,导入可能受影响: " + FirstLine(ex.Message));
}
var paths = await client.GetIterationPathsAsync(_cfg.Project, Log);
var keep = _cmbIteration.Text;
_cmbIteration.Items.Clear();
foreach (var p in paths) _cmbIteration.Items.Add(p);
if (_cmbIteration.Items.Count > 0)
{
int match = -1;
for (int i = 0; i < _cmbIteration.Items.Count; i++)
{
if (string.Equals(_cmbIteration.Items[i].ToString(), keep, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_cmbIteration.Items[i].ToString(), _cfg.IterationPath, StringComparison.OrdinalIgnoreCase))
{
match = i;
break;
}
}
_cmbIteration.SelectedIndex = match >= 0 ? match : 0;
}
Log($"已加载 {_cmbIteration.Items.Count} 个迭代路径");
SetConnection("已连接", ChipKind.Ok);
SetBadgeOf(_cardConn, BadgeKind.Done);
ShowError(_errIteration, _fIteration, _cardData, "");
}
}
catch (Exception ex)
{
SetConnection("连接失败", ChipKind.Danger);
SetBadgeOf(_cardConn, BadgeKind.Warn);
ShowError(_errIteration, _fIteration, _cardData, "迭代列表加载失败,可手填完整路径");
Log("加载迭代失败: " + FirstLine(ex.Message));
Log("排查: 内网请确认「使用系统代理」保持关闭;DNS 失败多为 VPN 或未连内网。");
}
finally
{
SetBusy(false);
OnFieldChanged();
}
}
@@ -369,14 +275,14 @@ namespace TfsExcelImporter
{
ExcelService.CopyTemplate(dlg.FileName);
Log("模板已导出: " + dlg.FileName);
MessageBox.Show(this, "模板已导出:\n" + dlg.FileName, "完成",
MessageBoxButtons.OK, MessageBoxIcon.Information);
_txtExcel.Text = dlg.FileName;
SaveUiToConfig();
PreviewData();
}
catch (Exception ex)
{
Log("导出模板失败: " + ex.Message);
MessageBox.Show(this, "导出模板失败:\n" + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
Log("导出模板失败: " + FirstLine(ex.Message));
ShowError(_errExcel, _fExcel, _cardData, "导出模板失败:" + FirstLine(ex.Message));
}
}
}
@@ -386,7 +292,8 @@ namespace TfsExcelImporter
var path = _txtExcel.Text.Trim();
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
MessageBox.Show(this, "请先选择有效的 Excel 文件", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errExcel, _fExcel, _cardData, "请选择有效的 Excel 文件(.xlsx)");
Append("预览未执行:Excel 文件不存在", LogLevel.Warn);
return;
}
try
@@ -396,23 +303,31 @@ namespace TfsExcelImporter
_grid.DataSource = _rows.Select((r, i) => new
{
行号 = i + 1,
r.Title,
r.State,
标题 = r.Title,
状态 = r.State,
指派 = r.AssignedTo,
财务项目 = r.FinancialProject,
r.Completed,
LogDate = r.LogDate,
r.PbiId,
r.Description
完成量 = r.Completed,
日期 = r.LogDate,
父PBI = r.PbiId,
说明 = r.Description
}).ToList();
ShowError(_errExcel, _fExcel, _cardData, "");
StyleGridColumns();
_lblStatus.Text = $"已读取 {_rows.Count} 行";
Log($"读取 {path} 共 {_rows.Count} 行");
if (_rows.Count == 0) Log("警告: 没有可导入的数据行(Title 为空的行会被忽略)");
SetBadgeOf(_cardData, BadgeKind.Done);
SetBadgeOf(_cardPreview, _rows.Count > 0 ? BadgeKind.Done : BadgeKind.Warn);
UpdateEmptyState();
RefreshSummaries();
SaveUiToConfig();
}
catch (Exception ex)
{
Log("读取 Excel 失败: " + ex.Message);
MessageBox.Show(this, "读取 Excel 失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
Log("读取 Excel 失败: " + FirstLine(ex.Message));
ShowError(_errExcel, _fExcel, _cardData, "读取失败:" + FirstLine(ex.Message));
UpdateEmptyState();
}
}
@@ -422,13 +337,15 @@ namespace TfsExcelImporter
SaveUiToConfig();
if (string.IsNullOrWhiteSpace(_cfg.IterationPath))
{
MessageBox.Show(this, "请先选择目标 Iteration", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errIteration, _fIteration, _cardData, "修 Area 需要先选择目标 Iteration");
Append("按迭代修 Area 取消:目标迭代未选", LogLevel.Warn);
return;
}
if (string.IsNullOrWhiteSpace(_cfg.AreaPath))
{
MessageBox.Show(this, "请先在 config.json 中设置 AreaPath(如 Tools_Dev\\DataTechnologies)", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errArea, _fArea, _cardAdv, "请填写 Area 路径,例如 Tools_Dev\\DataTechnologies");
OpenDetail(_cardAdv);
_txtAreaPath.Focus();
return;
}
var confirm = MessageBox.Show(this,
@@ -436,7 +353,8 @@ namespace TfsExcelImporter
"确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (confirm != DialogResult.Yes) return;
_btnFixArea.Enabled = false;
SetBusy(true);
OpenDetail(_cardLog);
try
{
using (var client = new TfsClient(_cfg))
@@ -447,19 +365,16 @@ namespace TfsExcelImporter
: $"Area 修正完成,共 {r.Item1} 条";
_lblStatus.Text = summary;
Log(summary);
MessageBox.Show(this, summary, r.Item2 > 0 ? "部分完成" : "完成",
MessageBoxButtons.OK, r.Item2 > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
ShowError(_errArea, _fArea, _cardAdv, "");
}
}
catch (Exception ex)
{
Log("Area 修正失败: " + ex.Message);
MessageBox.Show(this, "Area 修正失败:\n" + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
Log("Area 修正失败: " + FirstLine(ex.Message));
}
finally
{
_btnFixArea.Enabled = true;
SetBusy(false);
}
}
@@ -467,15 +382,17 @@ namespace TfsExcelImporter
private async Task LinkParentAsync()
{
SaveUiToConfig();
int parentId;
if (string.IsNullOrWhiteSpace(_cfg.IterationPath))
{
MessageBox.Show(this, "请先选择目标 Iteration", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errIteration, _fIteration, _cardData, "补挂父项需要先选择目标 Iteration");
Append("补挂父项取消:目标迭代未选", LogLevel.Warn);
return;
}
if (!int.TryParse(_cfg.ParentPbiId, out var parentId))
if (!int.TryParse(_cfg.ParentPbiId, out parentId))
{
MessageBox.Show(this, "请先在「父PBI Id」输入框填入父工作项 Id(纯数字)", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errParent, _fParent, _cardData, "父 PBI Id 需为纯数字,例如 219975");
_txtParentPbi.Focus();
return;
}
var confirm = MessageBox.Show(this,
@@ -483,7 +400,8 @@ namespace TfsExcelImporter
"确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (confirm != DialogResult.Yes) return;
_btnLinkParent.Enabled = false;
SetBusy(true);
OpenDetail(_cardLog);
try
{
using (var client = new TfsClient(_cfg))
@@ -493,20 +411,17 @@ namespace TfsExcelImporter
var summary = $"补挂完成: 挂接 {r.Item1} / 已有父跳过 {r.Item2} / 失败 {r.Item3}";
_lblStatus.Text = summary;
Log(summary);
MessageBox.Show(this, summary + "\n挂接后在积压工作页展开父项即可看到子项。",
r.Item3 > 0 ? "部分完成" : "完成", MessageBoxButtons.OK,
r.Item3 > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
Log("挂接后在积压工作页展开父项即可看到子项。");
ShowError(_errParent, _fParent, _cardData, "");
}
}
catch (Exception ex)
{
Log("补挂父项失败: " + ex.Message);
MessageBox.Show(this, "补挂父项失败:\n" + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
Log("补挂父项失败: " + FirstLine(ex.Message));
}
finally
{
_btnLinkParent.Enabled = true;
SetBusy(false);
}
}
@@ -517,14 +432,15 @@ namespace TfsExcelImporter
if (string.IsNullOrWhiteSpace(_cfg.IterationPath))
{
MessageBox.Show(this, "请先选择目标 Iteration(可点击“加载迭代列表”)", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errIteration, _fIteration, _cardData, "请先选择目标 Iteration(可点「加载迭代列表」,也可手填完整路径)");
Append("导入取消:目标迭代未选", LogLevel.Warn);
return;
}
var path = _txtExcel.Text.Trim();
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
MessageBox.Show(this, "请先选择有效的 Excel 文件", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errExcel, _fExcel, _cardData, "请选择有效的 Excel 文件(.xlsx)");
Append("导入取消:Excel 文件不存在", LogLevel.Warn);
return;
}
if (_rows.Count == 0)
@@ -532,18 +448,25 @@ namespace TfsExcelImporter
try { _rows = ExcelService.ReadRows(path); }
catch (Exception ex)
{
MessageBox.Show(this, "读取 Excel 失败:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
ShowError(_errExcel, _fExcel, _cardData, "读取失败:" + FirstLine(ex.Message));
Log("读取 Excel 失败: " + FirstLine(ex.Message));
return;
}
}
if (_rows.Count == 0)
{
MessageBox.Show(this, "Excel 中没有可导入的数据行", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
ShowError(_errExcel, _fExcel, _cardData, "Excel 中没有可导入的数据行");
Append("导入取消:没有可导入的数据行", LogLevel.Warn);
return;
}
_btnImport.Enabled = false;
_btnPreview.Enabled = false;
ShowError(_errIteration, _fIteration, _cardData, "");
ShowError(_errExcel, _fExcel, _cardData, "");
SetRunBadge(BadgeKind.Active);
SetBusy(true);
OpenDetail(_cardLog);
_pbar.Maximum = _rows.Count;
_pbar.Value = 0;
int ok = 0, skip = 0, fail = 0;
var report = new StringBuilder();
report.AppendLine("行号,Title,结果,WorkItemId,错误");
@@ -554,6 +477,8 @@ namespace TfsExcelImporter
{
Log("测试连接...");
Log(await client.TestConnectionAsync());
SetConnection("已连接", ChipKind.Ok);
SetBadgeOf(_cardConn, BadgeKind.Done);
// 字段解析
var fieldMap = await client.GetFieldMapAsync();
@@ -636,31 +561,40 @@ namespace TfsExcelImporter
report.AppendLine($"{line},\"{row.Title}\",FAIL,,\"{ex.Message.Replace("\"", "'")}\"");
Log($"[{i + 1}/{_rows.Count}] FAIL {row.Title} -> {ex.Message}");
}
_lblStatus.Text = $"导入中 {i + 1}/{_rows.Count}...";
_pbar.Value = Math.Min(i + 1, _pbar.Maximum);
_lblStatus.Text = $"{i + 1}/{_rows.Count}";
SetChips(ok, skip, fail, false);
}
SetRunBadge(fail > 0 ? BadgeKind.Warn : BadgeKind.Done);
}
}
catch (Exception ex)
{
Log("导入中断: " + ex.Message);
MessageBox.Show(this, "导入中断:\n" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
SetConnection("连接失败", ChipKind.Danger);
SetRunBadge(BadgeKind.Warn);
Log("导入中断: " + FirstLine(ex.Message));
Log("排查: 内网请确认「使用系统代理」保持关闭;DNS 失败多为 VPN 或未连内网。");
}
finally
{
_btnImport.Enabled = true;
_btnPreview.Enabled = true;
SetBusy(false);
try
{
var reportPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"import_report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv");
File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8);
Log($"报告已保存: {reportPath}");
_lastReportPath = reportPath;
Log("报告已保存: " + reportPath);
}
catch (Exception ex) { Log("写报告失败: " + ex.Message); }
_lblStatus.Text = $"完成: 成功 {ok} / 跳过 {skip} / 失败 {fail}";
Log($"汇总: 成功 {ok} / 跳过 {skip} / 失败 {fail}");
MessageBox.Show(this, $"导入完成。\n成功 {ok} / 跳过 {skip} / 失败 {fail}\n详见日志与报告 CSV。",
"完成", MessageBoxButtons.OK, MessageBoxIcon.Information);
catch (Exception ex) { Log("写报告失败: " + FirstLine(ex.Message)); }
_pbar.Value = _pbar.Maximum;
_lblStatus.Text = $"完成 {ok + skip + fail}/{_rows.Count}";
var summary = $"汇总: 成功 {ok} / 跳过 {skip} / 失败 {fail}";
_lblStatus.Text = summary;
Log(summary);
SetChips(ok, skip, fail, false);
RefreshSummaries();
if (fail == 0 && ok > 0) OpenDetail(_cardLog);
}
}
}
+889
View File
@@ -0,0 +1,889 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace TfsExcelImporter
{
/// <summary>
/// 界面外壳:ribbon 状态条 + 步骤卡片(1 连接 / 2 选数据 / 3 预览)+ 操作条 + 日志抽屉 + 高级工具抽屉。
/// 手风琴规则——同时只展开一个细节区,预览表格永远拿得到剩余高度。
/// 只负责摆放控件与状态呈现,TFS/Excel 调用逻辑仍在 MainForm.cs。
/// 所有字面量都是 96dpi 设计值,经 Theme.Px() 按系统缩放换算。
/// </summary>
public partial class MainForm
{
// ------------------------------------------------------------- 控件
private TextBox _txtUrl = new TextBox();
private TextBox _txtProject = new TextBox();
private TextBox _txtType = new TextBox();
private ComboBox _cmbAuth = new ComboBox();
private TextBox _txtUser = new TextBox();
private TextBox _txtPwd = new TextBox { UseSystemPasswordChar = true };
private TextBox _txtDomain = new TextBox();
private CheckBox _chkProxy = new CheckBox();
private ComboBox _cmbIteration = new ComboBox();
private TextBox _txtExcel = new TextBox();
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();
private CheckBox _chkBypass = new CheckBox();
private DataGridView _grid = new DataGridView();
private Button _btnPreview = new Button();
private Button _btnImport = new Button();
private Button _btnFixArea = new Button();
private Button _btnLinkParent = new Button();
private Button _btnTest = new Button();
private Button _btnClearLog = new Button();
private Button _btnOpenReport = new Button();
private CheckBox _chkAutoScroll = new CheckBox();
private LogView _log = new LogView();
private Label _lblStatus = new Label();
private ProgressTrack _pbar = new ProgressTrack();
// 容器与状态呈现
private Surface _ribbon;
private Card _cardConn, _cardData, _cardPreview, _cardLog, _cardAdv;
private Surface _actionBar;
private StepBadge _badgeRun;
private InputField _fUrl, _fProject, _fType, _fAuth, _fUser, _fPwd, _fDomain,
_fIteration, _fExcel, _fParent, _fArea;
private Label _errIteration, _errExcel, _errParent, _errArea;
private EmptyState _lblEmpty;
private StatusChip _chipConn, _chipOk, _chipSkip, _chipFail;
private readonly List<Stretch> _stretches = new List<Stretch>();
private readonly List<RightAnchor> _rightAnchors = new List<RightAnchor>();
private readonly List<Action> _layoutExtras = new List<Action>();
private BadgeKind _runBadge = BadgeKind.Pending;
// 页边距与区块间距:96dpi 设计值,取值时按缩放换算
private static int M { get { return Theme.Px(16); } }
private static int G { get { return Theme.Px(8); } }
private static int LogBodyH { get { return Theme.Px(150); } }
private string _lastReportPath = "";
/// <summary>需要在窗口拉宽时跟着变宽的控件。</summary>
private class Stretch
{
public Control C;
public Card Owner;
public int X, Y, Right, Bottom;
public bool FillHeight;
}
/// <summary>贴在卡片右缘的按钮:字段变宽后必须跟着右移,否则会压在字段上。</summary>
private class RightAnchor
{
public Control C;
public Card Owner;
public int FromRight;
public int Y;
}
// ------------------------------------------------------------- 构建
private void BuildUi()
{
Text = "TFS 2013 工作项 Excel 导入工具";
AutoScaleMode = AutoScaleMode.None; // 手工布局,缩放全部走 Theme.Px()
ClientSize = new Size(Theme.Px(1088), Theme.Px(800));
MinimumSize = new Size(Theme.Px(1000), Theme.Px(720));
StartPosition = FormStartPosition.CenterScreen;
Font = Theme.Body;
BackColor = Theme.Surface;
DoubleBuffered = true;
BuildRibbon();
BuildCardConn();
BuildCardData();
BuildCardPreview();
BuildActionBar();
BuildCardLog();
BuildCardAdv();
Controls.Add(_ribbon);
Controls.Add(_cardConn);
Controls.Add(_cardData);
Controls.Add(_cardPreview);
Controls.Add(_actionBar);
Controls.Add(_cardLog);
Controls.Add(_cardAdv);
Resize += (s, e) => Relayout();
_uiReady = true;
Relayout();
// 卡片 1 默认展开:首次打开就告诉用户从哪儿开始
_cardConn.Expanded = true;
_cardData.Expanded = false;
_cardLog.Expanded = false;
_cardAdv.Expanded = false;
foreach (var c in DetailCards()) c.ApplyHeight();
Relayout();
AcceptButton = _btnTest;
}
private IEnumerable<Card> DetailCards()
{
yield return _cardConn;
yield return _cardData;
yield return _cardLog;
yield return _cardAdv;
}
private Card MakeCard(int step, string title, bool collapsible, int headerH)
{
var card = new Card
{
StepNumber = step,
Title = title,
Collapsible = collapsible,
ShowBadge = step > 0,
HeaderH = headerH,
Expanded = false,
TabStop = false
};
card.OnToggle = expanded =>
{
if (expanded)
{
foreach (var other in DetailCards())
if (!ReferenceEquals(other, card)) { other.Expanded = false; other.ApplyHeight(); }
}
card.ApplyHeight();
Relayout();
};
return card;
}
// --- 顶部状态条 ---
private class RibGroup { public Label Cap; public Label Val; public Panel Sep; }
private readonly List<RibGroup> _ribItems = new List<RibGroup>();
private void BuildRibbon()
{
_ribbon = new Surface
{
LineColor = Theme.PrimarySoftBorder,
FaceColor = Theme.PrimarySoft,
Height = Theme.Px(44)
};
AddRibbonGroup("服务器", true);
AddRibbonGroup("项目", true);
AddRibbonGroup("工作项", false);
_chipConn = new StatusChip { BackColor = Theme.PrimarySoft };
_ribbon.Controls.Add(_chipConn);
}
/// <summary>一组「标题 + 值」;值文本会变长,所以位置每次重排时按实测宽度算。</summary>
private void AddRibbonGroup(string caption, bool withSeparator)
{
var cap = new Label
{
Text = caption,
Font = Theme.Caption,
ForeColor = Theme.Ink2,
AutoSize = true,
BackColor = Theme.PrimarySoft
};
var val = new Label
{
Text = "",
Font = Theme.BodyBold,
ForeColor = Theme.Ink,
AutoSize = true,
BackColor = Theme.PrimarySoft
};
_ribbon.Controls.Add(cap);
_ribbon.Controls.Add(val);
Panel sep = null;
if (withSeparator)
{
sep = new Panel { BackColor = Color.FromArgb(0xC2, 0xD3, 0xEF) };
_ribbon.Controls.Add(sep);
}
_ribItems.Add(new RibGroup { Cap = cap, Val = val, Sep = sep });
}
private void LayoutRibbon()
{
int x = M + Theme.Px(6);
foreach (var group in _ribItems)
{
var cap = group.Cap;
var val = group.Val;
var sep = group.Sep;
cap.SetBounds(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);
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));
x += Theme.Px(5);
}
_chipConn.SetBounds(_ribbon.Width - _chipConn.Width - Theme.Px(12),
(_ribbon.Height - _chipConn.Height) / 2, _chipConn.Width, _chipConn.Height);
}
// --- 步骤 1:连接 ---
private void BuildCardConn()
{
_cardConn = MakeCard(1, "连接 TFS 服务器", true, Theme.Px(44));
_cmbAuth.DropDownStyle = ComboBoxStyle.DropDownList;
_cmbAuth.Items.AddRange(new object[] { "ntlm", "basic", "default" });
_fUrl = NewField(_txtUrl, 620, true);
_fProject = NewField(_txtProject, 150, false);
_fType = NewField(_txtType, 180, false);
_fAuth = NewField(_cmbAuth, 170, false);
_fDomain = NewField(_txtDomain, 150, false);
_fUser = NewField(_txtUser, 180, false);
_fPwd = NewField(_txtPwd, 200, true);
_btnTest = NewButton("测试连接", BtnKind.Secondary, IconKind.Bolt, 130);
_chkProxy = NewCheck("使用系统代理", "内网 TFS 请保持关闭,勾选后常被代理拦成 502");
int y = _cardConn.HeaderH + Theme.S12;
// 三行表单共用一条字段左缘:标签列宽取各行最大值,同列字段宽度归一
int w1 = MaxLabelWidth("服务器 URL", "团队项目", "域");
int w2 = MaxLabelWidth("工作项类型", "用户");
int w3 = MaxLabelWidth("认证方式", "密码");
y = Row(_cardConn, y, 42, new Lbl("服务器 URL", w1), _fUrl);
Track(_fUrl, _cardConn, M, 0);
y = Row(_cardConn, y, 42, new Lbl("团队项目", w1), _fProject,
new Lbl("工作项类型", w2), _fType, new Lbl("认证方式", w3), _fAuth);
y = Row(_cardConn, y, 42, new Lbl("域", w1), _fDomain,
new Lbl("用户", w2), _fUser, new Lbl("密码", w3), _fPwd, G, _btnTest);
y = Row(_cardConn, y, 26, _chkProxy);
_cardConn.BodyH = y - _cardConn.HeaderH + Theme.S8;
}
// --- 步骤 2:迭代与数据源 ---
private void BuildCardData()
{
_cardData = MakeCard(2, "选择迭代与数据源", true, Theme.Px(44));
_cmbIteration.DropDownStyle = ComboBoxStyle.DropDown; // 允许手填完整路径
_fIteration = NewField(_cmbIteration, 500, false);
_fExcel = NewField(_txtExcel, 500, true);
_fParent = NewField(_txtParentPbi, 150, true);
_btnLoadIterations = NewButton("加载迭代列表", BtnKind.Secondary, IconKind.Sync, 130);
_btnBrowse = NewButton("浏览…", BtnKind.Secondary, IconKind.Folder, 92);
_btnTemplate = NewButton("导出 Excel 模板", BtnKind.Secondary, IconKind.Doc, 142);
_chkSkip = NewCheck("跳过已存在标题", "");
_chkBypass = NewCheck("bypassRules(绕过字段校验规则)", "");
// 三行字段对齐同一条左缘:标签列宽取最大值
int wLbl = MaxLabelWidth("目标迭代", "Excel 文件", "父 PBI Id");
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);
y = Row(_cardData, y, 52, new Lbl("Excel 文件", wLbl), _fExcel);
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);
y = Row(_cardData, y, 42, new Lbl("父 PBI Id", wLbl), _fParent, "Task 默认挂到该 backlog item;Excel 里加 PBI Id 列可逐行覆盖");
y = Row(_cardData, y, 24, _chkSkip, 2 * G, _chkBypass);
_errIteration = AddErrorUnder(_cardData, _fIteration);
_errExcel = AddErrorUnder(_cardData, _fExcel);
_errParent = AddErrorUnder(_cardData, _fParent);
_cardData.BodyH = y - _cardData.HeaderH + Theme.S8;
}
/// <summary>错误提示紧贴对应输入框下方;宽度随卡片重排(构建时卡片还没定宽)。</summary>
private Label AddErrorUnder(Card card, InputField field)
{
var err = NewError();
err.Height = Theme.Px(20);
card.Controls.Add(err);
TrackPlaced(err, card, field.Left, field.Bottom + Theme.Px(1), M, Theme.Px(18));
return err;
}
// --- 步骤 3:预览 ---
private void BuildCardPreview()
{
_cardPreview = MakeCard(3, "预览核对", false, Theme.Px(44));
_cardPreview.Expanded = true;
_cardPreview.BodyH = Theme.Px(300);
GridStyle.Apply(_grid);
_grid.AccessibleName = "Excel 预览数据";
_cardPreview.Controls.Add(_grid);
_lblEmpty = new EmptyState
{
Text = "还没有数据。到上面第 2 步选一个 .xlsx,或点「导出 Excel 模板」拿默认模板。",
Visible = false
};
_cardPreview.Controls.Add(_lblEmpty);
int gy = _cardPreview.HeaderH + Theme.S8;
TrackPlaced(_grid, _cardPreview, M, gy, M, Theme.S12);
TrackPlaced(_lblEmpty, _cardPreview, M, gy, M, Theme.S12);
_stretches[_stretches.Count - 1].FillHeight = true;
_stretches[_stretches.Count - 2].FillHeight = true;
}
// --- 步骤 4:操作条 ---
private void BuildActionBar()
{
_actionBar = new Surface { Height = Theme.Px(62) };
// 透明背景会让父容器改画自己的 BackColor(灰),这里直接给成操作条的白
_badgeRun = new StepBadge { BackColor = Theme.Card };
_btnPreview = NewButton("预览数据", BtnKind.Secondary, IconKind.Table, 120);
_btnImport = NewButton("开始导入 TFS", BtnKind.Primary, IconKind.ArrowUp, 176);
// 主/次按钮同高(层级靠主色表达,不再用 38px 拔高),避免操作条里两个按钮大小不齐
_pbar = new ProgressTrack
{
Minimum = 0,
Maximum = 100,
Value = 0,
Height = Theme.Px(8)
};
_chipOk = new StatusChip { BackColor = Theme.Card };
_chipSkip = new StatusChip { BackColor = Theme.Card };
_chipFail = new StatusChip { BackColor = Theme.Card };
_lblStatus = new Label
{
Text = "就绪",
Font = Theme.Caption,
ForeColor = Theme.Ink2,
TextAlign = ContentAlignment.MiddleRight,
AutoSize = false,
AutoEllipsis = true
};
_actionBar.Controls.Add(_badgeRun);
_actionBar.Controls.Add(_btnPreview);
_actionBar.Controls.Add(_btnImport);
_actionBar.Controls.Add(_pbar);
_actionBar.Controls.Add(_chipOk);
_actionBar.Controls.Add(_chipSkip);
_actionBar.Controls.Add(_chipFail);
_actionBar.Controls.Add(_lblStatus);
_layoutExtras.Add(LayoutActionBar);
SetChips(0, 0, 0, true);
}
private void LayoutActionBar()
{
int badge = Theme.Px(22);
int cy = (_actionBar.Height - badge) / 2;
_badgeRun.SetBounds(M + Theme.Px(2), cy, badge, badge);
_badgeRun.Set(4, _runBadge);
_btnPreview.SetBounds(M + Theme.Px(42), (_actionBar.Height - _btnPreview.Height) / 2,
_btnPreview.Width, _btnPreview.Height);
_btnImport.SetBounds(_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);
int statW = Theme.Px(160);
_lblStatus.SetBounds(_chipOk.Left - statW - G, 0, statW, _actionBar.Height);
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);
}
// --- 日志抽屉 ---
private void BuildCardLog()
{
_cardLog = MakeCard(0, "导入日志", true, Theme.Px(36));
_cardLog.ShowBadge = false;
_cardLog.HeaderReserve = Theme.Px(300); // 头部右侧留给 自动滚动/清空/打开报告
_cardLog.BodyH = LogBodyH;
_cardLog.SetSummary("尚未运行");
_log.AutoScrollToBottom = true;
_cardLog.Controls.Add(_log);
TrackPlaced(_log, _cardLog, M, _cardLog.HeaderH + Theme.Px(6), M, Theme.Px(6)).FillHeight = true;
_chkAutoScroll = new CheckBox
{
Text = "自动滚动",
Font = Theme.Caption,
ForeColor = Theme.Ink2,
AutoSize = false, // AutoSize 会按内容改高度,布局里就没办法按中线精确居中了
BackColor = Theme.Card,
Checked = true
};
_chkAutoScroll.CheckedChanged += (s, e) => _log.AutoScrollToBottom = _chkAutoScroll.Checked;
_btnClearLog = NewButton("清空", BtnKind.Ghost, IconKind.Trash, 62);
_btnClearLog.Height = Theme.Px(26);
_btnClearLog.Click += (s, e) => { _log.ClearLog(); Append("日志已清空", LogLevel.Info); };
_btnOpenReport = NewButton("打开报告 CSV", BtnKind.Ghost, IconKind.Doc, 124);
_btnOpenReport.Height = Theme.Px(26);
_btnOpenReport.Click += (s, e) => OpenReport();
_cardLog.Controls.Add(_btnOpenReport);
_cardLog.Controls.Add(_btnClearLog);
_cardLog.Controls.Add(_chkAutoScroll);
_layoutExtras.Add(() =>
{
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,
_btnOpenReport.Width, _btnOpenReport.Height);
_btnClearLog.SetBounds(_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);
});
}
// --- 高级工具抽屉 ---
private void BuildCardAdv()
{
_cardAdv = MakeCard(0, "高级工具", true, Theme.Px(36));
_cardAdv.ShowBadge = false;
_fArea = NewField(_txtAreaPath, 420, true);
_btnFixArea = NewButton("按迭代修 Area", BtnKind.Secondary, IconKind.Wrench, 158);
_btnLinkParent = NewButton("补挂父项", BtnKind.Secondary, IconKind.ArrowUp, 120);
int y = _cardAdv.HeaderH + Theme.S12;
y = Row(_cardAdv, y, 52, "Area 路径", _fArea, "改 Area 与补挂父项都按上面的「目标迭代」筛选已有工作项");
y = Row(_cardAdv, y, 34, _btnFixArea, G, _btnLinkParent, G, "已关闭的工作项会被临时重新打开,完成后自动还原状态");
_errArea = AddErrorUnder(_cardAdv, _fArea);
_cardAdv.BodyH = y - _cardAdv.HeaderH + Theme.S8;
}
// ------------------------------------------------------------- 摆放助手
/// <summary>带固定列宽的行内标签:同一张卡片的多行表单用它把字段左缘对齐成一列。</summary>
private class Lbl
{
public readonly string Text;
public readonly int MinWidth; // 运行时像素(构建时已含缩放)
public Lbl(string text, int minWidth) { Text = text; MinWidth = minWidth; }
}
/// <summary>一组标签文字的最大实测宽度,用作对齐列宽。</summary>
private static int MaxLabelWidth(params string[] texts)
{
int w = 0;
foreach (var t in texts) w = Math.Max(w, Theme.TextWidth(t, Theme.Body));
return w;
}
/// <summary>行内元素:string=标签,Lbl=定宽标签,int=已换算好的间距,Control=控件(宽度取控件自身设置)。</summary>
private int Row(Card card, int y, int designStride, params object[] items)
{
int stride = Theme.Px(designStride);
int x = M;
string lastLabel = null;
foreach (var item in items)
{
var text = item as string;
var spec = item as Lbl;
if (text != null || spec != null)
{
string label = text ?? spec.Text;
var lbl = new Label
{
Text = label,
Font = Theme.Body,
ForeColor = Theme.Ink2,
AutoSize = true,
BackColor = Theme.Card
};
lbl.SetBounds(x, y + (Theme.FieldH - lbl.PreferredHeight) / 2, lbl.PreferredWidth, lbl.PreferredHeight);
card.Controls.Add(lbl);
x += Math.Max(lbl.PreferredWidth, spec != null ? spec.MinWidth : 0) + Theme.Px(6);
lastLabel = label;
continue;
}
if (item is int) { x += (int)item; continue; }
var c = item as Control;
if (c == null) continue;
int top = y + Math.Max(0, (Theme.FieldH - c.Height) / 2);
c.SetBounds(x, top, c.Width, c.Height);
card.Controls.Add(c);
if (string.IsNullOrEmpty(c.AccessibleName) && lastLabel != null)
c.AccessibleName = lastLabel.TrimEnd(':', ':');
x += c.Width + Theme.S12;
}
return y + stride;
}
private InputField NewField(Control inner, int designWidth, bool mono)
{
var field = new InputField { Width = Theme.Px(designWidth) };
field.Add(inner, mono);
return field;
}
private FlatButton NewButton(string text, BtnKind kind, IconKind glyph, int designMinWidth)
{
var btn = new FlatButton { Text = text, Kind = kind, Glyph = glyph };
btn.FitWidth(designMinWidth); // 宽度按文字+图标量出来,参数只是下限
return btn;
}
private CheckBox NewCheck(string text, string hint)
{
var chk = new CheckBox
{
Text = text,
Font = Theme.Body,
ForeColor = Theme.Ink2,
AutoSize = true,
BackColor = Theme.Card
};
if (!string.IsNullOrEmpty(hint))
{
var h = new Label
{
Text = " · " + hint,
Font = Theme.Caption,
ForeColor = Theme.Ink3,
AutoSize = true,
BackColor = Theme.Card
};
hintFor(chk, h);
}
return chk;
}
/// <summary>复选框后跟随的灰色说明:与复选框同批显示/隐藏。</summary>
private void hintFor(CheckBox chk, Label hint)
{
chk.HandleCreated += (s, e) =>
{
chk.Parent.Controls.Add(hint);
hint.SetBounds(chk.Right + Theme.Px(2), chk.Top + (chk.Height - hint.PreferredHeight) / 2,
hint.PreferredWidth, hint.PreferredHeight);
hint.Visible = chk.Visible;
chk.VisibleChanged += (s2, e2) => hint.Visible = chk.Visible;
};
}
private Label NewError()
{
return new Label
{
Font = Theme.Caption,
ForeColor = Theme.Danger,
AutoSize = false,
Visible = false,
BackColor = Theme.Card,
TextAlign = ContentAlignment.MiddleLeft
};
}
/// <summary>横向随窗口变宽(X/Y 取控件摆放时的位置,Right 是留给右侧按钮的固定宽度)。</summary>
private void Track(Control c, Card owner, int right, int bottom)
{
_stretches.Add(new Stretch { C = c, Owner = owner, X = c.Left, Y = c.Top, Right = right, Bottom = bottom });
}
private Stretch TrackPlaced(Control c, Card owner, int x, int y, int right, int bottom)
{
var st = new Stretch { C = c, Owner = owner, X = x, Y = y, Right = right, Bottom = bottom };
_stretches.Add(st);
return st;
}
/// <summary>控件右缘贴卡片右内边距,FromRight 为距右缘的固定量。</summary>
private void AnchorRight(Control c, Card owner, int fromRight, int y)
{
if (c.Parent != owner) owner.Controls.Add(c);
_rightAnchors.Add(new RightAnchor { C = c, Owner = owner, FromRight = fromRight, Y = y });
}
// ------------------------------------------------------------- 重排
private bool _inLayout;
private bool _uiReady;
/// <summary>手风琴:只展开指定细节区,其余收成一行摘要。</summary>
private void OpenDetail(Card card)
{
foreach (var c in DetailCards())
{
bool open = ReferenceEquals(c, card);
if (c.Expanded != open)
{
c.Expanded = open;
c.ApplyHeight();
}
}
Relayout();
}
private void Relayout()
{
// 构建过程中各区块还不齐全,过早重排会踩到 null
if (!_uiReady || _inLayout) return;
_inLayout = true;
try { DoLayout(); }
finally { _inLayout = false; }
}
private void DoLayout()
{
int cw = Math.Max(Theme.Px(320), ClientSize.Width - M * 2);
_ribbon.SetBounds(M, M, cw, _ribbon.Height);
int y = _ribbon.Bottom + G;
foreach (var card in new[] { _cardConn, _cardData })
{
card.SetBounds(M, y, cw, card.Height);
y = card.Bottom + G;
}
// 从底部往上排
int by = ClientSize.Height - M;
_cardAdv.SetBounds(M, by - _cardAdv.Height, cw, _cardAdv.Height);
by = _cardAdv.Top - G;
_cardLog.SetBounds(M, by - _cardLog.Height, cw, _cardLog.Height);
by = _cardLog.Top - G;
_actionBar.SetBounds(M, by - _actionBar.Height, cw, _actionBar.Height);
by = _actionBar.Top - G;
// 预览卡吃剩余高度;空间不足时先让日志收成一行
if (by - G - y < Theme.Px(150) && _cardLog.Expanded)
{
_cardLog.Expanded = false;
_cardLog.ApplyHeight();
by = ClientSize.Height - M;
_cardAdv.SetBounds(M, by - _cardAdv.Height, cw, _cardAdv.Height);
by = _cardAdv.Top - G;
_cardLog.SetBounds(M, by - _cardLog.Height, cw, _cardLog.Height);
by = _cardLog.Top - G;
_actionBar.SetBounds(M, by - _actionBar.Height, cw, _actionBar.Height);
by = _actionBar.Top - G;
}
_cardPreview.SetBounds(M, y, cw, Math.Max(Theme.Px(120), by - G - y));
_cardPreview.BodyH = _cardPreview.Height - _cardPreview.HeaderH;
foreach (var st in _stretches)
{
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);
}
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);
}
LayoutRibbon();
foreach (var act in _layoutExtras) act();
_cardConn.Invalidate();
_cardData.Invalidate();
}
// ------------------------------------------------------------- 状态呈现
private void Append(string msg, LogLevel lvl)
{
_log.AppendLine(DateTime.Now.ToString("HH:mm:ss"), lvl, msg);
_cardLog.SetSummary(msg);
if (!_cardLog.Expanded && (lvl == LogLevel.Fail || lvl == LogLevel.Warn))
_cardLog.Invalidate();
}
private void SetConnection(string text, ChipKind kind)
{
_chipConn.Set(text, kind);
Relayout();
}
private void SetChips(int ok, int skip, int fail, bool neutral)
{
_chipOk.Set("成功 " + ok, neutral ? ChipKind.Quiet : ChipKind.Ok);
_chipSkip.Set("跳过 " + skip, neutral ? ChipKind.Quiet : (skip > 0 ? ChipKind.Warn : ChipKind.Ok));
_chipFail.Set("失败 " + fail, neutral ? ChipKind.Quiet : (fail > 0 ? ChipKind.Danger : ChipKind.Ok));
if (_actionBar != null) Relayout();
}
private void SetBadgeOf(Card card, BadgeKind kind)
{
card.SetBadge(kind);
card.Invalidate(new Rectangle(0, 0, card.Width, card.HeaderH));
}
private void ShowError(Label err, InputField field, Card card, string message)
{
if (err == null) return;
err.Text = message ?? "";
err.Visible = !string.IsNullOrEmpty(message);
field.Invalid = !string.IsNullOrEmpty(message);
card.SetInvalid(!string.IsNullOrEmpty(message));
if (!string.IsNullOrEmpty(message) && !card.Expanded) card.Toggle();
Relayout();
}
private void RefreshRibbon()
{
SetRibbonValue(0, ShortHost(_cfg != null ? _cfg.TfsUrl : ""));
SetRibbonValue(1, string.IsNullOrEmpty(_cfg.Project) ? "(未填)" : _cfg.Project);
SetRibbonValue(2, string.IsNullOrEmpty(_cfg.WorkItemType) ? "(未填)" : _cfg.WorkItemType);
Relayout();
}
private void SetRibbonValue(int group, string text)
{
var val = _ribItems[group].Val;
if (val.Text == text) return;
val.Text = text;
val.Invalidate();
}
private static string ShortHost(string url)
{
if (string.IsNullOrWhiteSpace(url)) return "(未填)";
try
{
var u = new Uri(url);
var path = (u.PathAndQuery ?? "").Trim('/');
var host = u.Host;
if (u.Port > 0 && u.Port != 80 && u.Port != 443) host += ":" + u.Port;
return string.IsNullOrEmpty(path) ? host : host + "/" + path.Split('/')[0];
}
catch { return url; }
}
private void RefreshSummaries()
{
var iter = _cmbIteration.Text.Trim();
var file = _txtExcel.Text.Trim();
var fileName = string.IsNullOrEmpty(file) ? "" : System.IO.Path.GetFileName(file);
_cardConn.SetSummary(string.Join(" · ", new[]
{
ShortHost(_txtUrl.Text.Trim()),
_txtProject.Text.Trim(),
_txtType.Text.Trim(),
_cmbAuth.SelectedItem != null ? _cmbAuth.SelectedItem.ToString() : "ntlm"
}));
_cardData.SetSummary(string.Join(" · ", new[]
{
string.IsNullOrEmpty(iter) ? "迭代未选" : iter,
string.IsNullOrEmpty(fileName) ? "未选 Excel" : fileName,
_rows.Count > 0 ? _rows.Count + " 行" : "未预览",
string.IsNullOrWhiteSpace(_txtParentPbi.Text) ? "父 PBI 未填" : "父 PBI #" + _txtParentPbi.Text.Trim()
}));
_cardPreview.SetSummary(_rows.Count > 0
? "共 " + _rows.Count + " 行,可直接开始导入"
: "选择 Excel 后在这里核对将要写入的内容");
_cardAdv.SetSummary(string.IsNullOrWhiteSpace(_txtAreaPath.Text)
? "Area 未设置"
: "Area " + _txtAreaPath.Text.Trim());
}
/// <summary>第 4 步徽标:待办 / 进行中 / 完成 / 有失败。</summary>
private void SetRunBadge(BadgeKind kind)
{
_runBadge = kind;
_badgeRun.Set(4, kind);
}
/// <summary>绑定后调一次:长文本列吃剩余宽度,数字列等宽右对齐,表头不换行。</summary>
private void StyleGridColumns()
{
foreach (DataGridViewColumn col in _grid.Columns)
{
col.SortMode = DataGridViewColumnSortMode.NotSortable;
col.HeaderCell.Style.WrapMode = DataGridViewTriState.False;
var head = col.HeaderText;
if (head == "标题" || head == "说明")
{
col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
col.MinimumWidth = Theme.Px(head == "标题" ? 280 : 160);
}
else
{
col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
}
if (head == "行号" || head == "完成量" || head == "日期" || head == "父PBI")
{
col.DefaultCellStyle.Font = Theme.Mono;
col.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
}
}
}
private void UpdateEmptyState()
{
bool hasRows = _grid.RowCount > 0;
_lblEmpty.Visible = !hasRows;
_grid.Visible = hasRows;
if (_lblEmpty.Visible) _lblEmpty.BringToFront();
}
private void OpenReport()
{
if (string.IsNullOrEmpty(_lastReportPath) || !System.IO.File.Exists(_lastReportPath))
{
Append("还没有可打开的报告 CSV", LogLevel.Warn);
return;
}
try { System.Diagnostics.Process.Start(_lastReportPath); }
catch (Exception ex) { Append("打开报告失败: " + ex.Message, LogLevel.Fail); }
}
private void SetBusy(bool busy)
{
UseWaitCursor = busy;
_btnImport.Enabled = !busy;
_btnPreview.Enabled = !busy;
_btnTest.Enabled = !busy;
_btnLoadIterations.Enabled = !busy;
_btnFixArea.Enabled = !busy;
_btnLinkParent.Enabled = !busy;
_btnBrowse.Enabled = !busy;
_btnTemplate.Enabled = !busy;
}
}
/// <summary>预览空状态:表格矢量图标 + 提示文字,整块在预览区居中,替代单行灰字。</summary>
internal class EmptyState : Control
{
public EmptyState()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
BackColor = Theme.Card;
TabStop = false;
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
int icon = Theme.Px(40);
int textH = Theme.Px(18);
int blockH = icon + Theme.Px(10) + textH;
int top = Math.Max(Theme.Px(4), (Height - blockH) / 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);
g.SmoothingMode = old;
TextRenderer.DrawText(g, Text, Theme.Body,
new Rectangle(Theme.Px(16), top + icon + Theme.Px(10), Width - Theme.Px(32), textH),
Theme.Ink3,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter
| TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis);
}
protected override void OnTextChanged(EventArgs e) { base.OnTextChanged(e); Invalidate(); }
}
}
+2
View File
@@ -9,6 +9,8 @@
<AssemblyTitle>TFS Excel 导入工具</AssemblyTitle>
<Product>TFS Excel 导入工具</Product>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<!-- System DPI 感知:界面像素尺寸按 Theme.Px() 换算,见 app.manifest -->
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="TfsExcelImporter.app"/>
<!--
System DPI 感知:进程启动时按系统缩放取一次 DPI,Theme.Px() 据此换算所有像素尺寸。
刻意不用 PerMonitorV2——本程序没有跨显示器迁移场景,而运行中改缩放需要重建整套控件树。
改了 Windows 显示缩放比例后需重启程序才会按新比例重排。
-->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">System</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10/11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
+2
View File
@@ -0,0 +1,2 @@
[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)