refactor: 插件 UI 从 WinForms 迁移到 CycleGUI,并修复代码质量问题

将 StandardScene 各插件的配置/监控窗体从 WinForms 迁移到 CycleGUI(删除 .Designer.cs/.resx,重写为 PanelBuilder 立即模式 UI,新增 CycleUiHelper 统一对话框)。

同时修复代码审核中的问题:
- 后台文件写入加锁 + try/catch(ButtonBoxManager / DoorManager,对齐 LoopViewer.SaveTasks 模式)
- CoderFieldsMetadata.cs 启用 #nullable enable,消除 CS8632 警告
- DummyCar 移除已废弃的 rightClickAction()/SetPosition()
- CarRemoteHelper.OpenVehicleWebPage 的 Process.Start 加 try/catch
- 重命名名不副实的 Mstsc()(现为打开网页)
- 统一弃元命名为 _
- TrafficInterlockViewer 改用稳定 Id(GUID)做选择/编辑,替代行索引
- csproj 改用 $(CGUILibDir) 解析 CycleGUI,绝对路径收敛到 Directory.Build.props

构建:dotnet build StandardScene.sln → 0 错误,30 警告(均为历史遗留)。
注:static 单例状态重构(审核第 8 项)暂未处理,留待单独任务。
This commit is contained in:
zhaowei.huang
2026-06-26 15:00:53 +08:00
parent c8e540d272
commit a0dc1e6cd0
91 changed files with 3946 additions and 15419 deletions
+278 -534
View File
@@ -1,595 +1,339 @@
using Newtonsoft.Json;
using StandardScene.Model;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using System.Threading.Tasks;
using CycleGUI;
using Newtonsoft.Json;
using SimpleCore.Library;
using StandardScene.Model;
using StandardScene.Utils;
namespace LoopViewerApp
{
public partial class LoopViewer : Form
/// <summary>
/// 环线/循环任务配置管理界面(CycleGUI 版,替代原 WinForms <c>LoopViewer</c> 窗体)。
/// <list type="bullet">
/// <item>维护 <c>tasklist.json</c><see cref="List{T}"/> of <see cref="LoopTask"/>)的增 / 改 / 删;与 <c>AbstractLoopMission</c> 读取同一文件。</item>
/// <item>单实例:再次打开则把已有面板置前。</item>
/// <item>勾选多行后「删除选中」可批量删除(保留原 ListView 多选删除能力);每行「编辑」按钮打开编辑对话框。</item>
/// <item>文件写入放后台线程,绝不阻塞渲染线程(避免界面卡死)。</item>
/// </list>
/// 沿用 <c>DeliveryViewer</c> 的同套模式(单实例面板、<c>pb.Table</c>、<c>CycleUiHelper.ConfirmThen</c>),不另造轮子。
/// 保留可实例化 + <see cref="Show"/> 以兼容既有调用 <c>new LoopViewer().Show()</c>。
/// </summary>
public class LoopViewer
{
private readonly string jsonPath =
Path.Combine(Application.StartupPath, "tasklist.json");
private const string TableId = "loop-task-list";
private List<LoopTask> tasks = new List<LoopTask>();
// 与 AbstractLoopMission 完全一致的读取路径,保证“写哪儿、它就读哪儿”。
private static string JsonPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tasklist.json");
// -1 表示新增模式;>=0 表示正在编辑对应索引
private int editingIndex = -1;
private static readonly object SaveLock = new object();
// 直接取自枚举,自动与 TaskKind / TaskStartType 保持同步(含 Charge),无需手写列表。
private static readonly string[] KindNames = Enum.GetNames(typeof(TaskKind));
private static readonly string[] StartTypeNames = Enum.GetNames(typeof(TaskStartType));
public LoopViewer()
private static Panel _panel;
private static Panel _dialog; // 新增/编辑对话框,限单实例
private static List<LoopTask> _tasks = new List<LoopTask>(); // 仅渲染线程读写
private static readonly HashSet<int> _selected = new HashSet<int>(); // 仅渲染线程读写,存被勾选任务的 Id
private static volatile string _status = "";
/// <summary>打开(或置前)任务管理面板。兼容原 <c>new LoopViewer().Show()</c> 调用方式。</summary>
public void Show() => Open();
/// <summary>打开(或置前)任务管理面板。</summary>
public static void Open()
{
InitializeComponent();
if (_panel != null)
{
try
{
_panel.BringToFront();
return;
}
catch
{
_panel = null;
}
}
if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
_selected.Clear();
LoadTasks();
var panel = GUI.DeclarePanel()
.ShowTitle("任务列表管理器")
.SetDefaultDocking(Panel.Docking.None)
.InitSize(1080, 620)
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_panel = panel;
panel.IfTerminalQuit(() => _panel = null);
panel.Define(pb =>
{
if (pb.Closing())
{
panel.Exit();
_panel = null;
return;
}
if (pb.Button("新增任务", distinct: "loop-add"))
OpenEditDialog(null);
pb.SameLine(12);
if (pb.Button("删除选中", distinct: "loop-del-selected"))
ConfirmDeleteSelected();
pb.SameLine(16);
pb.Label($"共 {_tasks.Count} 个任务,已选 {_selected.Count} 个");
pb.Table(TableId,
new[] { "选择", "ID", "任务类别", "当前站点", "目标站点", "流量控制", "优先级", "途径点", "启动类型", "操作" },
_tasks.Count, (row, i) =>
{
var t = _tasks[i];
var id = t.Id;
var sel = _selected.Contains(id);
if (row.Checkbox(ref sel, "勾选以批量删除"))
{
if (sel) _selected.Add(id);
else _selected.Remove(id);
}
row.Label($"{t.Id}");
row.Label($"{t.Kind}");
row.Label($"{t.CurrentStationId}");
row.Label($"{t.TargetStationId}");
row.Label($"{t.TrafficControl}");
row.Label($"{t.Priority}");
row.Label(t.IsViaPoint ? "是" : "否");
row.Label($"{t.StartType}");
if (row.ButtonGroup(new[] { "编辑" }, new[] { "编辑该任务" }) == 0)
OpenEditDialog(t);
}, height: 18, enableSearch: true);
if (!string.IsNullOrEmpty(_status))
{
pb.Separator();
pb.Label(_status);
}
// 事件驱动为主,配合较慢的节流重绘即可保证后台保存结果/状态及时反映。
pb.Panel.Repaint(repaintTimeMs: 500);
});
}
/// <summary>对选中项发起二次确认后删除(保留原多选删除的提示文案)。</summary>
private static void ConfirmDeleteSelected()
{
if (_selected.Count == 0)
{
_status = "未选择任何任务";
_panel?.Repaint();
return;
// 应用 ChargeStationManagementForm 风格的运行时样式调整
ApplyChargeStyle();
EnsureComboItems();
// 启用多选并绑定右键菜单与 Delete 键删除功能
try
{
if (lstTasks != null)
{
lstTasks.MultiSelect = true;
// 右键菜单:删除
var ctx = new ContextMenuStrip();
ctx.Items.Add("删除", null, (s, e) => OnDeleteSelectedTasks());
lstTasks.ContextMenuStrip = ctx;
// 键盘删除键绑定
lstTasks.KeyDown += lstTasks_KeyDown;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoopViewer context menu init error: {ex}");
}
try
{
InitOrLoadJson();
RenderListView();
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoopViewer initialization error: {ex}");
}
string prompt;
if (_selected.Count == 1)
prompt = $"确认删除任务 ID={_selected.First()}";
else
prompt = $"确认删除所选 {_selected.Count} 个任务?";
// 删除仅做内存列表增删(极快,可在渲染线程执行);真正的文件写入在 SaveTasks 内部放后台线程。
CycleUiHelper.ConfirmThen(prompt, DeleteSelected);
}
private void lstTasks_KeyDown(object sender, KeyEventArgs e)
private static void DeleteSelected()
{
try
{
if (e.KeyCode == Keys.Delete)
{
OnDeleteSelectedTasks();
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"lstTasks_KeyDown error: {ex}");
}
var removed = _tasks.RemoveAll(t => _selected.Contains(t.Id));
_selected.Clear();
SaveTasks();
_status = $"已删除 {removed} 个任务";
_panel?.Repaint();
}
/// <summary>
/// 删除 ListView 中选中的任务(支持多选)
/// 打开「新增 / 编辑」对话框(置顶非模态、限单实例)。<paramref name="existing"/> 为 null 表示新增,否则编辑该任务(保留其 Id)。
/// 每次打开都是全新面板:<c>defaultText</c> 能正确初始化,规避立即模式下文本框缓冲难以重置的问题。
/// </summary>
private void OnDeleteSelectedTasks()
private static void OpenEditDialog(LoopTask existing)
{
try
if (_dialog != null)
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0)
try { _dialog.BringToFront(); return; }
catch { _dialog = null; }
}
bool isAdd = existing == null;
int kindIdx = isAdd ? 0 : Math.Max(0, Array.IndexOf(KindNames, existing.Kind.ToString()));
int startIdx = Math.Max(0, Array.IndexOf(StartTypeNames,
(isAdd ? TaskStartType.AutoLoop : existing.StartType).ToString()));
string curText = (isAdd ? 0 : Clamp(existing.CurrentStationId, 0, 1000000)).ToString();
string tgtText = (isAdd ? 0 : Clamp(existing.TargetStationId, 0, 1000000)).ToString();
string trafficText = (isAdd ? 0 : Clamp(existing.TrafficControl, 0, 1000)).ToString();
string priText = (isAdd ? 1 : Clamp(existing.Priority, 0, 100)).ToString();
bool via = !isAdd && existing.IsViaPoint;
string err = "";
// 不用 Modal:原生「模态弹窗 + 标题栏关闭X」的 EndPopup 配对 bug 会断言崩溃。
// 也不用 TopMost:置顶视口带 NoAutoMerge,会让 DropdownBox 的下拉弹窗落到独立非置顶视口里、被对话框挡在后面(看不到选项)。
// 故采用与 DeliveryViewer 相同的普通浮动面板(非模态、不停靠):Begin/End 路径,X 关闭干净,下拉弹窗 z 序正常。
var dlg = GUI.DeclarePanel()
.ShowTitle(isAdd ? "新增任务" : $"编辑任务 ID: {existing.Id}")
.SetDefaultDocking(Panel.Docking.None)
.InitSize(420, 380)
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_dialog = dlg;
dlg.IfTerminalQuit(() => _dialog = null);
dlg.Define(pb =>
{
if (pb.Closing())
{
dlg.Exit();
_dialog = null;
return;
// 收集被选中的索引并按降序删除,避免索引移动问题
var selectedIndices = lstTasks.SelectedIndices.Cast<int>().OrderByDescending(i => i).ToList();
// 构造确认提示
string prompt;
if (selectedIndices.Count == 1)
{
int idx = selectedIndices[0];
if (idx >= 0 && idx < tasks.Count)
prompt = $"确认删除任务 ID={tasks[idx].Id}";
else
prompt = "确认删除选中任务?";
}
else
{
prompt = $"确认删除所选 {selectedIndices.Count} 个任务?";
}
if (MessageBox.Show(prompt, "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
// 控件 id 由 ImHashStr(prompt) 经 Encoding.ASCII 计算:中文会被压成 '?',导致同字数纯中文标签
// (如“当前站点/目标站点”“任务类别/启动类型”)哈希相同而抛 Duplicated id。故各标签加唯一 ASCII 序号前缀以区分。
pb.DropdownBox("1. 任务类别", KindNames, ref kindIdx);
var (c, _) = pb.TextInput("2. 当前站点 (0~1000000)", curText, alwaysReturnString: true);
curText = c;
var (tg, _) = pb.TextInput("3. 目标站点 (0~1000000)", tgtText, alwaysReturnString: true);
tgtText = tg;
var (tf, _) = pb.TextInput("4. 流量控制 (0~1000)", trafficText, alwaysReturnString: true);
trafficText = tf;
var (pr, _) = pb.TextInput("5. 优先级 (0~100)", priText, alwaysReturnString: true);
priText = pr;
pb.CheckBox("6. 途径点", ref via);
pb.DropdownBox("7. 启动类型", StartTypeNames, ref startIdx);
// 删除任务
foreach (var idx in selectedIndices)
if (!string.IsNullOrEmpty(err))
{
if (idx >= 0 && idx < tasks.Count)
pb.Separator();
pb.Label(err);
}
pb.Separator();
if (pb.Button("保存", distinct: "loop-edit-save"))
{
if (!TryParseClamp(curText, 0, 1000000, out var cur)) { err = "当前站点需为 0~1000000 的整数"; return; }
if (!TryParseClamp(tgtText, 0, 1000000, out var tgt)) { err = "目标站点需为 0~1000000 的整数"; return; }
if (!TryParseClamp(trafficText, 0, 1000, out var traffic)) { err = "流量控制需为 0~1000 的整数"; return; }
if (!TryParseClamp(priText, 0, 100, out var pri)) { err = "优先级需为 0~100 的整数"; return; }
Enum.TryParse<TaskKind>(KindNames[kindIdx], out var kind);
Enum.TryParse<TaskStartType>(StartTypeNames[startIdx], out var st);
if (isAdd)
{
tasks.RemoveAt(idx);
}
}
// 如果被删除项包含当前正在编辑的项,退出编辑状态
if (editingIndex >= 0)
{
if (editingIndex >= tasks.Count || selectedIndices.Any(i => i == editingIndex))
{
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
_tasks.Add(new LoopTask
{
Id = GetNextTaskId(),
Kind = kind,
CurrentStationId = cur,
TargetStationId = tgt,
TrafficControl = traffic,
Priority = pri,
IsViaPoint = via,
StartType = st
});
_status = "已新增任务";
}
else
{
// 重新计算编辑索引在删除后的新位置
int removedBefore = selectedIndices.Count(i => i < editingIndex);
editingIndex -= removedBefore;
existing.Kind = kind;
existing.CurrentStationId = cur;
existing.TargetStationId = tgt;
existing.TrafficControl = traffic;
existing.Priority = pri;
existing.IsViaPoint = via;
existing.StartType = st;
_status = $"已保存任务 ID={existing.Id}";
}
}
// 持久化并刷新列表视图
Save();
RenderListView();
SaveTasks();
dlg.Exit();
_dialog = null;
_panel?.Repaint();
}
pb.SameLine(8);
if (pb.Button("取消", distinct: "loop-edit-cancel"))
{
dlg.Exit();
_dialog = null;
}
});
}
/// <summary>下一个可用任务 Id(当前最大 Id + 1,空表则为 1)。</summary>
private static int GetNextTaskId() => _tasks.Count == 0 ? 1 : _tasks.Max(t => t.Id) + 1;
private static void LoadTasks()
{
try
{
var path = JsonPath;
if (!File.Exists(path))
File.WriteAllText(path, "[]");
var text = File.ReadAllText(path);
_tasks = JsonConvert.DeserializeObject<List<LoopTask>>(text) ?? new List<LoopTask>();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"OnDeleteSelectedTasks error: {ex}");
MessageBox.Show("删除失败:" + ex.Message);
_tasks = new List<LoopTask>();
_status = "加载 tasklist.json 失败,详见日志";
Diagnosis.Post($"LoopViewer 加载 tasklist.json 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
/// <summary>
/// 将 LoopViewer 的运行时样式调整为与 ChargeStationManagementForm 接近的视觉风格:
/// - 全局字体设为微软雅黑
/// - 表头暖色替换为蓝色沉稳风格(和充电界面一致)
/// - 按钮字号、背景色与充电界面保持一致(保存/删除/取消)
/// - 列表视图设置为整行选择、无边框、交替背景等
/// 注意:不修改 Designer 文件,仅在运行时统一控件表现,避免破坏设计器生成代码。
/// </summary>
private void ApplyChargeStyle()
/// <summary>序列化在渲染线程完成(极快),文件写入放后台线程,避免阻塞渲染线程。</summary>
private static void SaveTasks()
{
string json;
try
{
// 窗体级设置
this.StartPosition = FormStartPosition.CenterScreen;
this.MinimumSize = new System.Drawing.Size(1327, 738);
this.Font = new Font("微软雅黑", 9F, FontStyle.Regular);
// 调整 ListView(如果存在)
if (lstTasks != null)
{
lstTasks.View = View.Details;
lstTasks.FullRowSelect = true;
lstTasks.GridLines = false;
lstTasks.HeaderStyle = ColumnHeaderStyle.Nonclickable;
lstTasks.OwnerDraw = true; // 已有自定义绘制
lstTasks.BackColor = Color.White;
lstTasks.ForeColor = Color.FromArgb(33, 33, 33);
// 多选由初始化时控制(这里不强制)
}
// 下拉框统一字体
if (cmbTaskKind != null) cmbTaskKind.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (cmbStartType != null) cmbStartType.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
// 数值输入框统一字体
if (numCurrent != null) numCurrent.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numTarget != null) numTarget.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numTraffic != null) numTraffic.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numPriority != null) numPriority.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
// 标签字体统一
if (lblEditingId != null) lblEditingId.Font = new Font("微软雅黑", 10F, FontStyle.Bold);
// 按钮风格:与 ChargeStationManagementForm 保持一致的视觉优先级
if (btnSave != null)
{
btnSave.BackColor = Color.LightBlue;
btnSave.ForeColor = Color.Black;
btnSave.Font = new Font("微软雅黑", 11F, FontStyle.Bold);
btnSave.FlatStyle = FlatStyle.Flat;
}
if (btnDelete != null)
{
btnDelete.BackColor = Color.LightCoral;
btnDelete.ForeColor = Color.Black;
btnDelete.Font = new Font("微软雅黑", 11F, FontStyle.Bold);
btnDelete.FlatStyle = FlatStyle.Flat;
}
if (btnCancel != null)
{
btnCancel.BackColor = SystemColors.Control;
btnCancel.ForeColor = Color.Black;
btnCancel.Font = new Font("微软雅黑", 11F, FontStyle.Regular);
btnCancel.FlatStyle = FlatStyle.Flat;
}
// 如果存在额外的操作按钮(例如在面板上),尝试统一风格(容错)
foreach (Control ctrl in this.Controls)
{
if (ctrl is Panel pnl)
{
pnl.Padding = new Padding(12);
}
else if (ctrl is Button btn)
{
// 已设置主要按钮,其他按钮使用中性风格
if (btn == btnSave || btn == btnDelete || btn == btnCancel) continue;
btn.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
}
}
json = JsonConvert.SerializeObject(_tasks, Formatting.Indented);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ApplyChargeStyle error: {ex}");
_status = "保存失败,详见日志";
Diagnosis.Post($"LoopViewer 序列化 tasklist.json 异常: {ExceptionFormatter.FormatEx(ex)}");
return;
}
}
private void EnsureComboItems()
{
try
var path = JsonPath;
Task.Run(() =>
{
if (cmbTaskKind != null && cmbTaskKind.Items.Count == 0)
try
{
cmbTaskKind.Items.AddRange(new object[] { "Loop", "BranchPoint", "JoinPoint" });
cmbTaskKind.SelectedIndex = 0;
lock (SaveLock)
File.WriteAllText(path, json);
}
if (cmbStartType != null && cmbStartType.Items.Count == 0)
catch (Exception ex)
{
cmbStartType.Items.AddRange(new object[] { "Api", "Plc", "ButtonBox", "AutoLoop" });
cmbStartType.SelectedIndex = 3;
_status = "保存失败,详见日志";
Diagnosis.Post($"LoopViewer 保存 tasklist.json 异常: {ExceptionFormatter.FormatEx(ex)}");
_panel?.Repaint();
}
// 确保下拉框字体一致(防止 Designer 未设置)
if (cmbTaskKind != null) cmbTaskKind.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (cmbStartType != null) cmbStartType.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
}
catch { }
});
}
#region /
private void InitOrLoadJson()
private static int Clamp(int v, int min, int max) => v < min ? min : (v > max ? max : v);
private static bool TryParseClamp(string s, int min, int max, out int value)
{
try
if (int.TryParse((s ?? "").Trim(), out value))
{
if (!File.Exists(jsonPath))
File.WriteAllText(jsonPath, "[]");
var text = File.ReadAllText(jsonPath);
tasks = JsonConvert.DeserializeObject<List<LoopTask>>(text) ?? new List<LoopTask>();
}
catch (Exception ex)
{
tasks = new List<LoopTask>();
System.Diagnostics.Debug.WriteLine($"Load tasks failed: {ex}");
value = Clamp(value, min, max);
return true;
}
value = min;
return false;
}
#endregion
#region ID
/// <summary>
/// 获取下一个可用的任务ID(当前最大ID + 1)
/// </summary>
/// <returns>新的任务ID</returns>
private int GetNextTaskId()
{
if (tasks == null || tasks.Count == 0)
return 1;
int maxId = tasks.Max(t => t.Id);
return maxId + 1;
}
#endregion
#region OwnerDraw +
private void lstTasks_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
try
{
// 与 ChargeStationManagementForm 表头保持一致的深蓝背景与白色加粗字体
using (var backBrush = new SolidBrush(Color.FromArgb(63, 81, 181))) // 深蓝(与 Charge 界面一致)
using (var textBrush = new SolidBrush(Color.White)) // 白色文字
using (var font = new Font("微软雅黑", 9, FontStyle.Bold))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
var sf = new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Near };
var rect = e.Bounds;
rect.Inflate(-8, 0);
e.Graphics.DrawString(e.Header.Text, font, textBrush, rect, sf);
// 分隔线
using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
{
e.Graphics.DrawLine(pen, e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
}
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
private void lstTasks_DrawItem(object sender, DrawListViewItemEventArgs e)
{
// 由 DrawSubItem 绘制全部内容以保证每列对齐
}
private void lstTasks_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
try
{
var item = e.Item;
bool selected = item.Selected;
Rectangle bounds = e.Bounds;
// 选中行颜色:与 ChargeStationManagementForm 保持一致的蓝色强调
Color selectedBack = Color.FromArgb(0, 120, 215);
Color selectedFore = Color.White;
// 非选中行交替背景
Color evenBack = Color.White;
Color oddBack = Color.FromArgb(250, 251, 253);
Color normalFore = Color.FromArgb(33, 33, 33);
// 填充背景
if (selected)
{
using (var selBrush = new SolidBrush(selectedBack))
{
e.Graphics.FillRectangle(selBrush, bounds);
}
}
else
{
using (var back = new SolidBrush(e.ItemIndex % 2 == 0 ? evenBack : oddBack))
{
e.Graphics.FillRectangle(back, bounds);
}
}
// 绘制文本(加一点内边距)
string text = e.SubItem.Text ?? string.Empty;
Color fore = selected ? selectedFore : normalFore;
TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;
Rectangle textRect = bounds;
textRect.Inflate(-6, 0);
using (var font = new Font("微软雅黑", 9))
{
TextRenderer.DrawText(e.Graphics, text, font, textRect, fore, flags);
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
#endregion
#region /
private void RenderListView()
{
try
{
if (lstTasks == null) return;
lstTasks.BeginUpdate();
lstTasks.Items.Clear();
foreach (var t in tasks)
{
var lvi = new ListViewItem(new[]
{
t.Id.ToString(), // ID 列
t.Kind.ToString(),
t.CurrentStationId.ToString(),
t.TargetStationId.ToString(),
t.TrafficControl.ToString(),
t.Priority.ToString(),
t.IsViaPoint ? "是" : "否",
t.StartType.ToString()
});
lstTasks.Items.Add(lvi);
}
lstTasks.EndUpdate();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"RenderListView failed: {ex}");
}
}
private void Save()
{
try
{
File.WriteAllText(jsonPath, JsonConvert.SerializeObject(tasks, Formatting.Indented));
}
catch (Exception ex)
{
MessageBox.Show("保存失败:" + ex.Message);
}
}
#endregion
#region /
private void UpdateSaveButtonText()
{
if (btnSave != null)
{
// 文案固定为"保存"
btnSave.Text = "保存";
}
}
private void btnSave_Click(object sender, EventArgs e)
{
try
{
// 从面板读取值,直接在界面内编辑/新增
Enum.TryParse<TaskKind>(cmbTaskKind?.SelectedItem?.ToString() ?? "Loop", out var kind);
Enum.TryParse<TaskStartType>(cmbStartType?.SelectedItem?.ToString() ?? "AutoLoop", out var st);
if (editingIndex >= 0 && editingIndex < tasks.Count)
{
// 更新模式:保留原有ID
var existingTask = tasks[editingIndex];
existingTask.Kind = kind;
existingTask.CurrentStationId = (int)(numCurrent?.Value ?? 0);
existingTask.TargetStationId = (int)(numTarget?.Value ?? 0);
existingTask.TrafficControl = (int)(numTraffic?.Value ?? 0);
existingTask.Priority = (int)(numPriority?.Value ?? 1);
existingTask.IsViaPoint = chkViaPoint?.Checked ?? false;
existingTask.StartType = st;
}
else
{
// 新增模式:自动分配新ID
var t = new LoopTask
{
Id = GetNextTaskId(), // 自增ID
Kind = kind,
CurrentStationId = (int)(numCurrent?.Value ?? 0),
TargetStationId = (int)(numTarget?.Value ?? 0),
TrafficControl = (int)(numTraffic?.Value ?? 0),
Priority = (int)(numPriority?.Value ?? 1),
IsViaPoint = chkViaPoint?.Checked ?? false,
StartType = st
};
tasks.Add(t);
}
Save();
RenderListView();
// 恢复新增状态
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnSave_Click error: {ex}");
MessageBox.Show("操作失败:" + ex.Message);
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
// 取消编辑,清空面板并回到"添加"模式
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
private void btnEdit_Click(object sender, EventArgs e)
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0) return;
int idx = lstTasks.SelectedIndices[0];
if (idx < 0 || idx >= tasks.Count) return;
editingIndex = idx;
LoadTaskToPanel(tasks[idx]);
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnEdit_Click error: {ex}");
}
}
private void btnDelete_Click(object sender, EventArgs e)
{
// 兼容旧的删除按钮:复用统一删除逻辑
OnDeleteSelectedTasks();
}
#endregion
#region
private void lstTasks_MouseDoubleClick(object sender, MouseEventArgs e)
{
try
{
if (lstTasks == null) return;
var item = lstTasks.GetItemAt(e.X, e.Y);
if (item == null) return;
int idx = item.Index;
if (idx < 0 || idx >= tasks.Count) return;
editingIndex = idx;
LoadTaskToPanel(tasks[idx]);
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"lstTasks_MouseDoubleClick error: {ex}");
}
}
#endregion
#region
private void LoadTaskToPanel(LoopTask t)
{
if (t == null) return;
try
{
// 显示当前编辑的任务ID(只读显示)
if (lblEditingId != null) lblEditingId.Text = $"编辑任务 ID: {t.Id}";
if (cmbTaskKind != null) cmbTaskKind.SelectedItem = t.Kind.ToString();
if (numCurrent != null) numCurrent.Value = Math.Max(numCurrent.Minimum, Math.Min(numCurrent.Maximum, t.CurrentStationId));
if (numTarget != null) numTarget.Value = Math.Max(numTarget.Minimum, Math.Min(numTarget.Maximum, t.TargetStationId));
if (numTraffic != null) numTraffic.Value = Math.Max(numTraffic.Minimum, Math.Min(numTraffic.Maximum, t.TrafficControl));
if (numPriority != null) numPriority.Value = Math.Max(numPriority.Minimum, Math.Min(numPriority.Maximum, t.Priority));
if (chkViaPoint != null) chkViaPoint.Checked = t.IsViaPoint;
if (cmbStartType != null) cmbStartType.SelectedItem = t.StartType.ToString();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoadTaskToPanel error: {ex}");
}
}
private void ClearPanelInputs()
{
try
{
// 清除编辑ID显示
if (lblEditingId != null) lblEditingId.Text = "新增任务";
if (cmbTaskKind != null) cmbTaskKind.SelectedIndex = 0;
if (numCurrent != null) numCurrent.Value = 0;
if (numTarget != null) numTarget.Value = 0;
if (numTraffic != null) numTraffic.Value = 0;
if (numPriority != null) numPriority.Value = 1;
if (chkViaPoint != null) chkViaPoint.Checked = false;
if (cmbStartType != null) cmbStartType.SelectedIndex = 3;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ClearPanelInputs error: {ex}");
}
}
#endregion
}
}
}