zcbot/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs

833 lines
32 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Text.Json;
namespace Zcbot.WindowsNode;
internal sealed class ConfigurationForm : Form
{
private const int ContentWidth = 760;
private readonly TextBox server = CreateTextBox("http://127.0.0.1:8765");
private readonly TextBox nodeName = CreateTextBox(Environment.MachineName.ToLowerInvariant());
private readonly TextBox enrollmentCode = CreateTextBox(usePassword: true);
private readonly Button register = CreateButton("注册并连接", 128, primary: true);
private readonly Button reconnect = CreateButton("立即重连", 112, primary: true);
private readonly Button resetIdentity = CreateButton("清除本机身份并重新注册", 220);
private readonly Button copyDiagnostics = CreateButton("复制诊断信息", 132);
private readonly Button runAnsysAcceptance = CreateButton("运行内置基准验收", 180, primary: true);
private readonly Button enableAnsysGate = CreateButton("开启 ANSYS 执行门", 164);
private readonly CheckBox startAtLogin = new()
{
Text = "登录 Windows 后自动启动节点",
AutoSize = true,
Margin = new Padding(0, 4, 0, 8),
};
private readonly Label state = new()
{
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 13, FontStyle.Bold),
Margin = new Padding(0, 2, 0, 5),
};
private readonly Label detail = CreateBodyLabel();
private readonly Label identity = CreateBodyLabel();
private readonly Label capabilitySummary = CreateBodyLabel();
private readonly Label ansysAcceptanceStatus = CreateBodyLabel();
private readonly ProgressBar ansysAcceptanceProgress = new()
{
Style = ProgressBarStyle.Marquee,
MarqueeAnimationSpeed = 30,
Height = 8,
Dock = DockStyle.Top,
Visible = false,
Margin = new Padding(0, 8, 0, 5),
};
private readonly Label jobSummary = CreateBodyLabel();
private readonly Label jobDetail = CreateBodyLabel();
private readonly DataGridView jobGrid = CreateJobGrid();
private readonly JobInboxStore jobInbox = new(NodePaths.ForCurrentMachine().JobsDirectory);
private readonly NodeAdapterRegistry adapters;
private readonly System.Windows.Forms.Timer jobRefreshTimer = new() { Interval = 1000 };
private readonly TableLayoutPanel registrationCard;
private readonly TableLayoutPanel ansysAcceptanceCard;
private bool changingStartup;
private NodeConfig? currentConfig;
private string? passedAcceptanceReport;
private CancellationTokenSource? acceptanceStop;
internal event Func<EnrollOptions, Task>? RegisterRequested;
internal event Action? ReconnectRequested;
internal event Action? ResetIdentityRequested;
internal ConfigurationForm()
{
adapters = NodeAdapterRegistry.CreateDefault(jobInbox);
Text = "zcbot Windows Node";
AutoScaleMode = AutoScaleMode.Dpi;
ClientSize = new Size(840, 680);
MinimumSize = new Size(820, 640);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Microsoft YaHei UI", 9);
FormBorderStyle = FormBorderStyle.Sizable;
BackColor = Color.FromArgb(241, 245, 249);
var shell = new TableLayoutPanel
{
Dock = DockStyle.Fill,
AutoScroll = true,
ColumnCount = 3,
RowCount = 1,
Padding = new Padding(0, 22, 0, 22),
BackColor = BackColor,
};
shell.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
shell.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, ContentWidth));
shell.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
Controls.Add(shell);
var page = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Dock = DockStyle.Top,
ColumnCount = 1,
RowCount = 7,
BackColor = BackColor,
};
page.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
shell.Controls.Add(page, 1, 0);
var heading = new TableLayoutPanel
{
AutoSize = true,
Dock = DockStyle.Top,
ColumnCount = 1,
Margin = new Padding(4, 0, 4, 18),
};
heading.Controls.Add(new Label
{
Text = "zcbot Windows Node",
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 20, FontStyle.Bold),
ForeColor = Color.FromArgb(15, 23, 42),
});
heading.Controls.Add(CreateHint("连接本机科研软件与 zcbot 的受控执行节点"));
page.Controls.Add(heading);
var statusCard = CreateCard();
statusCard.Controls.Add(CreateSectionTitle("节点"));
statusCard.Controls.Add(state);
statusCard.Controls.Add(detail);
statusCard.Controls.Add(identity);
statusCard.Controls.Add(CreateDivider());
foreach (var contract in NodeAdapterRegistry.InstalledContracts)
{
statusCard.Controls.Add(CreateCapabilityRow(contract.DisplayName, contract.Capability));
}
statusCard.Controls.Add(capabilitySummary);
var resetActions = CreateActions();
resetActions.Controls.Add(reconnect);
resetActions.Controls.Add(resetIdentity);
resetActions.Controls.Add(copyDiagnostics);
statusCard.Controls.Add(resetActions);
page.Controls.Add(statusCard);
ansysAcceptanceCard = CreateCard();
ansysAcceptanceCard.Controls.Add(CreateSectionTitle("ANSYS Mechanical 真机验收"));
ansysAcceptanceCard.Controls.Add(CreateHint(
"执行门关闭时也可本地验收。程序使用自带的标准试件,自动建立 fixed_face 和 load_face先验证完整进程树取消再连续求解 20 次并检查结果与进程释放。"));
ansysAcceptanceCard.Controls.Add(ansysAcceptanceStatus);
ansysAcceptanceCard.Controls.Add(ansysAcceptanceProgress);
var acceptanceActions = CreateActions();
acceptanceActions.Controls.Add(runAnsysAcceptance);
acceptanceActions.Controls.Add(enableAnsysGate);
ansysAcceptanceCard.Controls.Add(acceptanceActions);
page.Controls.Add(ansysAcceptanceCard);
var jobsCard = CreateCard();
jobsCard.Controls.Add(CreateSectionTitle("本机任务"));
jobsCard.Controls.Add(CreateHint(
"仅显示已经派发到本机的任务;状态来自本地持久化记录,断线或重启后仍可查看。"));
jobsCard.Controls.Add(jobSummary);
jobsCard.Controls.Add(jobGrid);
jobsCard.Controls.Add(jobDetail);
page.Controls.Add(jobsCard);
registrationCard = CreateCard();
registrationCard.Controls.Add(CreateSectionTitle("首次注册"));
registrationCard.Controls.Add(CreateHint(
"从 zcbot 管理后台生成注册码。注册码默认 10 分钟有效,成功注册一次后立即失效。"));
AddField(registrationCard, "zcbot 服务地址", server, "云端填写站点根地址;本机测试使用 http://127.0.0.1:8765");
AddField(registrationCard, "节点名称", nodeName, "必须与注册码限定的节点名称完全一致");
AddField(registrationCard, "一次性注册码", enrollmentCode, "格式类似 ZCN-…;仅用于首次注册,不会长期保存");
var registerActions = CreateActions();
registerActions.Controls.Add(register);
registrationCard.Controls.Add(registerActions);
page.Controls.Add(registrationCard);
var runtimeCard = CreateCard();
runtimeCard.Controls.Add(CreateSectionTitle("运行设置"));
runtimeCard.Controls.Add(startAtLogin);
runtimeCard.Controls.Add(CreateHint(
"关闭此窗口后,节点仍在系统托盘运行。右键托盘图标可以立即重连或退出。"));
page.Controls.Add(runtimeCard);
var securityNote = CreateHint(
"安全说明Node Token 由 Windows DPAPI 加密保存,不在界面显示,也不会写入日志。");
securityNote.Margin = new Padding(4, 8, 4, 0);
page.Controls.Add(securityNote);
register.Click += async (_, _) => await RegisterAsync();
reconnect.Click += (_, _) => ReconnectRequested?.Invoke();
resetIdentity.Click += (_, _) => ResetIdentity();
copyDiagnostics.Click += (_, _) => CopyDiagnostics();
runAnsysAcceptance.Click += async (_, _) => await RunAnsysAcceptanceAsync();
enableAnsysGate.Click += (_, _) => EnableAnsysGate();
startAtLogin.CheckedChanged += (_, _) => ToggleStartup();
FormClosing += (_, eventArgs) =>
{
if (eventArgs.CloseReason == CloseReason.UserClosing)
{
eventArgs.Cancel = true;
Hide();
}
};
startAtLogin.Checked = StartupRegistration.IsEnabled;
jobGrid.SelectionChanged += (_, _) => ShowSelectedJob();
jobRefreshTimer.Tick += (_, _) => RefreshJobs();
jobRefreshTimer.Start();
Disposed += (_, _) =>
{
acceptanceStop?.Cancel();
acceptanceStop?.Dispose();
jobRefreshTimer.Dispose();
};
var ansysAdapter = adapters.Find("ansys.mechanical.static_structural@v1");
ansysAcceptanceCard.Visible = ansysAdapter?.SupportsLocalAcceptance == true;
enableAnsysGate.Enabled = false;
UpdateAnsysAcceptanceStatus();
RefreshJobs();
ApplyStatus(NodeStatus.Create(NodeState.NotRegistered, "尚未注册"), null);
}
internal void ApplyStatus(NodeStatus status, NodeConfig? config)
{
currentConfig = config;
state.Text = status.State switch
{
NodeState.Online => "● 在线",
NodeState.Connecting => "● 正在连接",
NodeState.NotRegistered => "● 尚未注册",
NodeState.AuthenticationRequired => "● 需要重新注册",
NodeState.Offline => "● 离线",
_ => "● 已停止",
};
state.ForeColor = status.State switch
{
NodeState.Online => Color.ForestGreen,
NodeState.Connecting => Color.DarkOrange,
NodeState.NotRegistered or NodeState.AuthenticationRequired => Color.Firebrick,
_ => Color.DimGray,
};
detail.Text = $"{status.Message} {status.ChangedAt:HH:mm:ss}";
identity.Text = config is null
? "尚未建立节点身份"
: $"节点:{config.NodeName}\nNode ID{config.NodeId}\n服务{config.ServerUrl}";
capabilitySummary.Text = config is null
? "注册后启用"
: string.Join("\n", adapters.All.Select(FormatAdapterStatus));
copyDiagnostics.Enabled = config is not null;
UpdateAnsysAcceptanceStatus();
var registered = config is not null;
registrationCard.Visible = !registered;
reconnect.Visible = registered;
resetIdentity.Visible = registered;
register.Enabled = !registered && status.State != NodeState.Connecting;
if (registered)
{
server.Text = config!.ServerUrl.AbsoluteUri.TrimEnd('/');
nodeName.Text = config.NodeName;
enrollmentCode.Clear();
}
}
private static string FormatAdapterStatus(INodeAdapter adapter)
{
var runtime = adapter.DetectRuntime();
var softwareName = runtime.Software == "OriginPro" ? "Origin" : runtime.Software;
var softwareVersion = string.IsNullOrWhiteSpace(runtime.SoftwareVersion)
? $"{softwareName} 版本未知"
: $"{softwareName} {runtime.SoftwareVersion}";
var state = runtime.Health == "ready" ? "可用" : "不可用";
return $"{adapter.DisplayName}{state} · Adapter {adapter.AdapterVersion} · {softwareVersion}\n"
+ runtime.Detail;
}
private void CopyDiagnostics()
{
if (currentConfig is null)
{
return;
}
try
{
Clipboard.SetText(BuildDiagnosticText(currentConfig));
MessageBox.Show(
"诊断信息已复制,可直接粘贴到 Codex 对话。内容不包含 Node Token。",
"已复制", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (ExternalException exception)
{
MessageBox.Show(
$"无法写入剪贴板:{exception.Message}",
"复制失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private string BuildDiagnosticText(NodeConfig config)
{
var paths = NodePaths.ForCurrentMachine();
var builder = new StringBuilder()
.AppendLine("zcbot Windows Node diagnostics")
.AppendLine($"Node: {config.NodeName}")
.AppendLine($"Node ID: {config.NodeId}")
.AppendLine($"Server: {config.ServerUrl}")
.AppendLine($"Node version: {Application.ProductVersion}")
.AppendLine($"OS: {Environment.OSVersion.VersionString}")
.AppendLine($"Data root: {paths.RootDirectory}")
.AppendLine($"ANSYS gate: {AnsysGateState()}");
foreach (var adapter in adapters.All.OrderBy(item => item.Capability, StringComparer.Ordinal))
{
try
{
var runtime = adapter.DetectRuntime();
builder.AppendLine()
.AppendLine($"Capability: {adapter.Capability}")
.AppendLine($"Adapter: {adapter.AdapterVersion}")
.AppendLine($"Software: {runtime.Software} {runtime.SoftwareVersion ?? "unknown"}")
.AppendLine($"Health: {runtime.Health}")
.AppendLine($"Detail: {runtime.Detail}")
.AppendLine($"Runtime: {adapter.RuntimePath}");
}
catch (Exception exception) when (
exception is IOException
or InvalidOperationException
or UnauthorizedAccessException)
{
builder.AppendLine()
.AppendLine($"Capability: {adapter.Capability}")
.AppendLine($"Adapter: {adapter.AdapterVersion}")
.AppendLine($"Diagnostic error: {exception.Message}");
}
}
return builder.ToString();
}
private async Task RunAnsysAcceptanceAsync()
{
var adapter = adapters.Find("ansys.mechanical.static_structural@v1");
if (adapter?.SupportsLocalAcceptance != true)
{
MessageBox.Show(
"当前安装包没有 ANSYS 验收工具。请先安装新版完整 Node 包。",
"无法验收", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
using var outputDialog = new FolderBrowserDialog
{
Description = "选择保存 ANSYS 验收报告的父目录",
UseDescriptionForTitle = true,
ShowNewFolderButton = true,
};
if (outputDialog.ShowDialog(this) != DialogResult.OK)
{
return;
}
var answer = MessageBox.Show(
"程序会使用内置标准试件执行一次进程树取消和连续 20 次真实静力求解,期间会获取并释放 ANSYS 许可证,可能耗时较长。确认开始?",
"开始 ANSYS 真机验收", MessageBoxButtons.YesNo, MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (answer != DialogResult.Yes)
{
return;
}
var workRoot = Path.Combine(
outputDialog.SelectedPath,
$"zcbot-ansys-acceptance-{DateTime.Now:yyyyMMdd-HHmmss}");
acceptanceStop?.Dispose();
acceptanceStop = new CancellationTokenSource();
runAnsysAcceptance.Enabled = false;
enableAnsysGate.Enabled = false;
ansysAcceptanceProgress.Visible = true;
ansysAcceptanceStatus.ForeColor = Color.DarkOrange;
ansysAcceptanceStatus.Text = "正在启动真机验收…";
var progress = new Progress<string>(line =>
{
ansysAcceptanceStatus.Text = line;
});
try
{
var result = await adapter.RunLocalAcceptanceAsync(
workRoot,
progress,
acceptanceStop.Token);
passedAcceptanceReport = result.Passed ? result.ReportPath : null;
enableAnsysGate.Enabled = result.Passed && AnsysGateState() != "enabled";
ansysAcceptanceStatus.ForeColor = result.Passed ? Color.ForestGreen : Color.Firebrick;
ansysAcceptanceStatus.Text = result.Passed
? $"验收通过。报告:{result.ReportPath}\n请确认许可证管理端席位已释放再开启执行门。"
: $"验收未通过:{result.Detail}\n报告{result.ReportPath}";
}
catch (OperationCanceledException)
{
passedAcceptanceReport = null;
ansysAcceptanceStatus.ForeColor = Color.DimGray;
ansysAcceptanceStatus.Text = "验收已停止,执行门保持关闭。";
}
catch (Exception exception) when (
exception is IOException
or InvalidDataException
or InvalidOperationException
or JsonException
or UnauthorizedAccessException)
{
passedAcceptanceReport = null;
ansysAcceptanceStatus.ForeColor = Color.Firebrick;
ansysAcceptanceStatus.Text = $"验收启动或执行失败:{exception.Message}";
}
finally
{
ansysAcceptanceProgress.Visible = false;
runAnsysAcceptance.Enabled = true;
}
}
private void EnableAnsysGate()
{
if (passedAcceptanceReport is null || !AcceptanceReportPassed(passedAcceptanceReport))
{
MessageBox.Show(
"没有可验证的本次验收通过报告,执行门不会开启。",
"无法开启", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var answer = MessageBox.Show(
"请先在许可证管理端确认正常求解和取消后的席位均已释放。开启后需要完全退出并重新启动 Node 才会进入实际调度。确认开启机器级执行门?",
"开启 ANSYS 执行门", MessageBoxButtons.YesNo, MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (answer != DialogResult.Yes)
{
return;
}
try
{
Environment.SetEnvironmentVariable(
"ZCBOT_ANSYS_242_VALIDATED", "1", EnvironmentVariableTarget.Machine);
enableAnsysGate.Enabled = false;
ansysAcceptanceStatus.ForeColor = Color.ForestGreen;
ansysAcceptanceStatus.Text =
"机器级执行门已写入。请从托盘退出 Node 后重新启动;重启后 ANSYS 应显示可用。";
}
catch (Exception exception) when (
exception is SecurityException or UnauthorizedAccessException)
{
MessageBox.Show(
$"写入机器级环境变量需要管理员权限:{exception.Message}\n请退出 Node以管理员身份启动后再次点击。",
"权限不足", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private static bool AcceptanceReportPassed(string path)
{
try
{
using var document = JsonDocument.Parse(File.ReadAllBytes(path));
return document.RootElement.TryGetProperty("passed", out var passed)
&& passed.ValueKind == JsonValueKind.True;
}
catch (Exception exception) when (
exception is IOException or JsonException or UnauthorizedAccessException)
{
return false;
}
}
private void UpdateAnsysAcceptanceStatus()
{
if (!ansysAcceptanceCard.Visible || ansysAcceptanceProgress.Visible)
{
return;
}
var gate = AnsysGateState();
if (passedAcceptanceReport is not null
&& AcceptanceReportPassed(passedAcceptanceReport))
{
ansysAcceptanceStatus.ForeColor = Color.ForestGreen;
ansysAcceptanceStatus.Text = gate == "enabled"
? "当前执行门:已开启。请完全退出并重新启动 Node使新环境生效。"
: $"验收通过。报告:{passedAcceptanceReport}\n请确认许可证管理端席位已释放再开启执行门。";
enableAnsysGate.Enabled = gate != "enabled";
return;
}
ansysAcceptanceStatus.ForeColor = gate == "enabled"
? Color.ForestGreen
: Color.FromArgb(71, 85, 105);
ansysAcceptanceStatus.Text = gate == "enabled"
? "当前执行门已开启。Node 重启并且 probe 正常后可接收 ANSYS 任务。"
: "当前执行门:关闭。安装成功不代表验收通过,请先运行本页验收。";
enableAnsysGate.Enabled = false;
}
private static string AnsysGateState() =>
Environment.GetEnvironmentVariable(
"ZCBOT_ANSYS_242_VALIDATED", EnvironmentVariableTarget.Machine) == "1"
? "enabled"
: "disabled";
private void RefreshJobs()
{
Guid? selectedId = jobGrid.SelectedRows.Count > 0
&& jobGrid.SelectedRows[0].Tag is JobDisplaySnapshot selected
? selected.JobId
: null;
IReadOnlyList<JobDisplaySnapshot> snapshots;
try
{
snapshots = jobInbox.ReadJobSnapshots();
}
catch (Exception exception) when (
exception is IOException or UnauthorizedAccessException)
{
jobSummary.Text = $"无法读取本机任务:{exception.Message}";
return;
}
jobGrid.Rows.Clear();
DataGridViewRow? rowToSelect = null;
foreach (var snapshot in snapshots)
{
var index = jobGrid.Rows.Add(
FormatJobStage(snapshot.Stage),
snapshot.Title,
snapshot.InputFilename,
FormatProgress(snapshot),
snapshot.AcceptedAt.LocalDateTime.ToString("MM-dd HH:mm"),
snapshot.JobId.ToString("N")[..8]);
var row = jobGrid.Rows[index];
row.Tag = snapshot;
row.DefaultCellStyle.ForeColor = snapshot.Stage switch
{
"failed" => Color.Firebrick,
"cancelled" => Color.DimGray,
"succeeded" => Color.ForestGreen,
_ => Color.FromArgb(30, 41, 59),
};
if (snapshot.JobId == selectedId)
{
rowToSelect = row;
}
}
var active = snapshots.Count(item => item.IsActive);
jobSummary.Text = snapshots.Count == 0
? "暂无本机任务"
: $"活动任务 {active} 个 · 最近记录 {snapshots.Count} 条";
if (rowToSelect is not null)
{
rowToSelect.Selected = true;
jobGrid.CurrentCell = rowToSelect.Cells[0];
}
else if (jobGrid.Rows.Count > 0)
{
jobGrid.Rows[0].Selected = true;
jobGrid.CurrentCell = jobGrid.Rows[0].Cells[0];
}
else
{
jobDetail.Text = "选择任务后可查看执行阶段、更新时间和完整 Job ID。";
}
ShowSelectedJob();
}
private void ShowSelectedJob()
{
if (jobGrid.SelectedRows.Count == 0
|| jobGrid.SelectedRows[0].Tag is not JobDisplaySnapshot snapshot)
{
return;
}
jobDetail.Text = string.Join("\n", [
$"{FormatJobStage(snapshot.Stage)} · {snapshot.Detail}",
$"能力:{snapshot.Capability}",
$"Job ID{snapshot.JobId}",
$"更新时间:{snapshot.UpdatedAt.LocalDateTime:yyyy-MM-dd HH:mm:ss}",
]);
}
private static string FormatProgress(JobDisplaySnapshot snapshot) =>
snapshot.Stage == "software_running"
? $"已执行 {FormatElapsed(DateTimeOffset.UtcNow - snapshot.UpdatedAt)}"
: $"{snapshot.Progress}%";
private static string FormatElapsed(TimeSpan elapsed) => elapsed.TotalHours >= 1
? $"{(int)elapsed.TotalHours}:{elapsed.Minutes:00}:{elapsed.Seconds:00}"
: $"{elapsed.Minutes:00}:{elapsed.Seconds:00}";
private static string FormatJobStage(string stage) => stage switch
{
"accepted" => "等待处理",
"downloading_inputs" => "下载输入",
"ready_to_run" => "准备执行",
"software_running" => "软件执行中",
"uploading_outputs" => "上传结果",
"succeeded" => "成功",
"failed" => "失败",
"cancelled" => "已取消",
_ => stage,
};
private void ResetIdentity()
{
var answer = MessageBox.Show(
"这会删除本机加密身份,随后需要使用新注册码重新注册。请先在管理后台删除或禁用云端旧节点。是否继续?",
"清除本机节点身份", MessageBoxButtons.YesNo, MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (answer == DialogResult.Yes)
{
ResetIdentityRequested?.Invoke();
}
}
private async Task RegisterAsync()
{
if (RegisterRequested is null)
{
return;
}
try
{
register.Enabled = false;
detail.Text = "正在注册…";
var options = EnrollOptions.Parse([
"--server", server.Text, "--name", nodeName.Text, "--code", enrollmentCode.Text]);
await RegisterRequested(options);
}
catch (Exception exception) when (exception is NodeConfigurationException or HttpRequestException)
{
MessageBox.Show(exception.Message, "注册失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
register.Enabled = true;
}
}
private void ToggleStartup()
{
if (changingStartup)
{
return;
}
try
{
StartupRegistration.SetEnabled(startAtLogin.Checked);
}
catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
{
MessageBox.Show(exception.Message, "自动启动设置失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
changingStartup = true;
try
{
startAtLogin.Checked = StartupRegistration.IsEnabled;
}
finally
{
changingStartup = false;
}
}
}
private static TableLayoutPanel CreateCard()
{
var card = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Dock = DockStyle.Top,
ColumnCount = 1,
Padding = new Padding(20, 17, 20, 17),
Margin = new Padding(0, 0, 0, 14),
BackColor = Color.White,
};
card.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
return card;
}
private static Label CreateSectionTitle(string text) => new()
{
Text = text,
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 11, FontStyle.Bold),
ForeColor = Color.FromArgb(30, 41, 59),
Margin = new Padding(0, 0, 0, 8),
};
private static Label CreateBodyLabel() => new()
{
AutoSize = true,
Dock = DockStyle.Fill,
ForeColor = Color.FromArgb(71, 85, 105),
MaximumSize = new Size(ContentWidth - 40, 0),
Margin = new Padding(0, 2, 0, 5),
};
private static Label CreateHint(string text) => new()
{
Text = text,
AutoSize = true,
Dock = DockStyle.Fill,
ForeColor = Color.FromArgb(100, 116, 139),
Margin = new Padding(0, 2, 0, 4),
};
private static DataGridView CreateJobGrid()
{
var grid = new DataGridView
{
Height = 238,
Dock = DockStyle.Top,
Margin = new Padding(0, 8, 0, 8),
BackgroundColor = Color.White,
BorderStyle = BorderStyle.FixedSingle,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
AllowUserToResizeRows = false,
AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None,
ColumnHeadersHeight = 34,
EnableHeadersVisualStyles = false,
MultiSelect = false,
ReadOnly = true,
RowHeadersVisible = false,
RowTemplate = { Height = 34 },
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
};
grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(241, 245, 249);
grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(51, 65, 85);
grid.DefaultCellStyle.SelectionBackColor = Color.FromArgb(219, 234, 254);
grid.DefaultCellStyle.SelectionForeColor = Color.FromArgb(30, 64, 175);
grid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "状态",
Width = 105,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "任务",
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
MinimumWidth = 170,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "输入",
Width = 125,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "进度",
Width = 95,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "接收时间",
Width = 100,
});
grid.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "Job ID",
Width = 76,
});
return grid;
}
private static Panel CreateDivider() => new()
{
Height = 1,
Dock = DockStyle.Top,
BackColor = Color.FromArgb(226, 232, 240),
Margin = new Padding(0, 10, 0, 12),
};
private static Panel CreateCapabilityRow(string title, string protocol)
{
var row = new Panel
{
AutoSize = false,
Height = 36,
Dock = DockStyle.Top,
BackColor = Color.FromArgb(239, 246, 255),
Margin = new Padding(0, 0, 0, 5),
Padding = new Padding(11, 7, 11, 7),
};
row.Controls.Add(new Label
{
Text = $"{title} · {protocol}",
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold),
ForeColor = Color.FromArgb(29, 78, 216),
});
return row;
}
private static TextBox CreateTextBox(string text = "", bool usePassword = false) => new()
{
Text = text,
UseSystemPasswordChar = usePassword,
AutoSize = false,
Height = 34,
BorderStyle = BorderStyle.FixedSingle,
Font = new Font("Segoe UI", 10),
};
private static Button CreateButton(string text, int width, bool primary = false)
{
var button = new Button
{
Text = text,
AutoSize = false,
Size = new Size(width, 36),
FlatStyle = FlatStyle.Flat,
BackColor = primary ? Color.FromArgb(37, 99, 235) : Color.White,
ForeColor = primary ? Color.White : Color.FromArgb(51, 65, 85),
Margin = new Padding(0, 0, 10, 0),
};
button.FlatAppearance.BorderColor = primary
? Color.FromArgb(37, 99, 235)
: Color.FromArgb(203, 213, 225);
return button;
}
private static FlowLayoutPanel CreateActions() => new()
{
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
Dock = DockStyle.Fill,
Margin = new Padding(0, 10, 0, 0),
};
private static void AddField(
TableLayoutPanel layout, string label, Control control, string hint)
{
layout.Controls.Add(new Label
{
Text = label,
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold),
ForeColor = Color.FromArgb(51, 65, 85),
Margin = new Padding(0, 8, 0, 4),
});
control.Dock = DockStyle.Top;
control.Margin = new Padding(0, 0, 0, 2);
layout.Controls.Add(control);
layout.Controls.Add(CreateHint(hint));
}
}