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
@@ -12,7 +12,7 @@ using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleLite;
using System.Windows.Forms;
using StandardScene.Utils;
using Newtonsoft.Json;
namespace StandardScene.InterLock
@@ -297,7 +297,7 @@ namespace StandardScene.InterLock
var vv = await Program.UI.Input("请输入true/false", title, "true");
if (vv == null || !bool.TryParse(vv, out var allow))
{
MessageBox.Show("输入错误");
CycleUiHelper.Alert("错误", "输入错误");
return;
}
lock (sync) toManipulate[site.id] = allow;
@@ -10,7 +10,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace StandardScene.InterLock
{
@@ -60,7 +59,7 @@ namespace StandardScene.InterLock
{
try
{
string ConfigPath = Path.Combine(Application.StartupPath, "Config/traffic.json");
string ConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config", "traffic.json");
if (File.Exists(ConfigPath))
{
@@ -137,6 +136,11 @@ namespace StandardScene.InterLock
public class TrafficArea
{
/// <summary>
/// 区域稳定标识,用于界面编辑/批量删除时避免行索引错位。
/// </summary>
public string Id { get; set; }
/// <summary>
/// 区域名称
/// </summary>
@@ -1,325 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LoopViewerApp
{
partial class TrafficInterlockViewer
{
private System.ComponentModel.IContainer components = null;
private ListView lstTasks;
private GroupBox grpEdit;
private ColumnHeader colAreaName;
private ColumnHeader colSites;
private ColumnHeader colControlRight;
private ColumnHeader colIsOccupied;
private ColumnHeader colIsEnabled;
private Label lblAreaName;
private TextBox txtAreaName;
private Label lblStationIds;
private TextBox txtStationIds;
private Label lblControlRight;
private TextBox txtControlRight;
private Label lblIsOccupied;
private CheckBox chkIsOccupied;
private Label lblIsEnabled;
private CheckBox chkIsEnabled;
private Label lblEditingHint;
private Button btnSave;
private Button btnRefresh;
private Button btnNew;
private Button btnDelete;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.lstTasks = new System.Windows.Forms.ListView();
this.colAreaName = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colSites = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colControlRight = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colIsOccupied = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colIsEnabled = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.grpEdit = new System.Windows.Forms.GroupBox();
this.lblEditingHint = new System.Windows.Forms.Label();
this.lblAreaName = new System.Windows.Forms.Label();
this.txtAreaName = new System.Windows.Forms.TextBox();
this.lblStationIds = new System.Windows.Forms.Label();
this.txtStationIds = new System.Windows.Forms.TextBox();
this.lblControlRight = new System.Windows.Forms.Label();
this.txtControlRight = new System.Windows.Forms.TextBox();
this.lblIsOccupied = new System.Windows.Forms.Label();
this.chkIsOccupied = new System.Windows.Forms.CheckBox();
this.lblIsEnabled = new System.Windows.Forms.Label();
this.chkIsEnabled = new System.Windows.Forms.CheckBox();
this.btnSave = new System.Windows.Forms.Button();
this.btnRefresh = new System.Windows.Forms.Button();
this.btnNew = new System.Windows.Forms.Button();
this.btnDelete = new System.Windows.Forms.Button();
this.grpEdit.SuspendLayout();
this.SuspendLayout();
//
// lstTasks
//
this.lstTasks.BackColor = System.Drawing.Color.White;
this.lstTasks.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.colAreaName,
this.colSites,
this.colControlRight,
this.colIsOccupied,
this.colIsEnabled});
this.lstTasks.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(33)))), ((int)(((byte)(33)))));
this.lstTasks.FullRowSelect = true;
this.lstTasks.HideSelection = false;
this.lstTasks.Location = new System.Drawing.Point(12, 12);
this.lstTasks.Name = "lstTasks";
this.lstTasks.OwnerDraw = true;
this.lstTasks.Size = new System.Drawing.Size(760, 320);
this.lstTasks.TabIndex = 0;
this.lstTasks.UseCompatibleStateImageBehavior = false;
this.lstTasks.View = System.Windows.Forms.View.Details;
this.lstTasks.DrawColumnHeader += new System.Windows.Forms.DrawListViewColumnHeaderEventHandler(this.lstTasks_DrawColumnHeader);
this.lstTasks.DrawItem += new System.Windows.Forms.DrawListViewItemEventHandler(this.lstTasks_DrawItem);
this.lstTasks.DrawSubItem += new System.Windows.Forms.DrawListViewSubItemEventHandler(this.lstTasks_DrawSubItem);
this.lstTasks.SelectedIndexChanged += new System.EventHandler(this.lstTasks_SelectedIndexChanged);
//
// colAreaName
//
this.colAreaName.Text = "区域名称";
this.colAreaName.Width = 140;
//
// colSites
//
this.colSites.Text = "区域站点集合";
this.colSites.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colSites.Width = 280;
//
// colControlRight
//
this.colControlRight.Text = "控制权";
this.colControlRight.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colControlRight.Width = 120;
//
// colIsOccupied
//
this.colIsOccupied.Text = "是否被占用";
this.colIsOccupied.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colIsOccupied.Width = 100;
//
// colIsEnabled
//
this.colIsEnabled.Text = "是否启用";
this.colIsEnabled.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colIsEnabled.Width = 100;
//
// grpEdit
//
this.grpEdit.Controls.Add(this.lblEditingHint);
this.grpEdit.Controls.Add(this.lblAreaName);
this.grpEdit.Controls.Add(this.txtAreaName);
this.grpEdit.Controls.Add(this.lblStationIds);
this.grpEdit.Controls.Add(this.txtStationIds);
this.grpEdit.Controls.Add(this.lblControlRight);
this.grpEdit.Controls.Add(this.txtControlRight);
this.grpEdit.Controls.Add(this.lblIsOccupied);
this.grpEdit.Controls.Add(this.chkIsOccupied);
this.grpEdit.Controls.Add(this.lblIsEnabled);
this.grpEdit.Controls.Add(this.chkIsEnabled);
this.grpEdit.Controls.Add(this.btnSave);
this.grpEdit.Controls.Add(this.btnRefresh);
this.grpEdit.Controls.Add(this.btnNew);
this.grpEdit.Controls.Add(this.btnDelete);
this.grpEdit.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.grpEdit.Location = new System.Drawing.Point(12, 345);
this.grpEdit.Name = "grpEdit";
this.grpEdit.Size = new System.Drawing.Size(760, 165);
this.grpEdit.TabIndex = 1;
this.grpEdit.TabStop = false;
this.grpEdit.Text = "数据新增/编辑(点击表格行可在此查看并编辑该行数据)";
//
// lblEditingHint
//
this.lblEditingHint.AutoSize = true;
this.lblEditingHint.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.lblEditingHint.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(120)))), ((int)(((byte)(215)))));
this.lblEditingHint.Location = new System.Drawing.Point(12, 125);
this.lblEditingHint.Name = "lblEditingHint";
this.lblEditingHint.Size = new System.Drawing.Size(65, 19);
this.lblEditingHint.TabIndex = 0;
this.lblEditingHint.Text = "新增区域";
//
// lblAreaName
//
this.lblAreaName.AutoSize = true;
this.lblAreaName.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblAreaName.Location = new System.Drawing.Point(12, 28);
this.lblAreaName.Name = "lblAreaName";
this.lblAreaName.Size = new System.Drawing.Size(79, 20);
this.lblAreaName.TabIndex = 1;
this.lblAreaName.Text = "区域名称:";
//
// txtAreaName
//
this.txtAreaName.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtAreaName.Location = new System.Drawing.Point(100, 24);
this.txtAreaName.Name = "txtAreaName";
this.txtAreaName.Size = new System.Drawing.Size(200, 25);
this.txtAreaName.TabIndex = 2;
//
// lblStationIds
//
this.lblStationIds.AutoSize = true;
this.lblStationIds.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblStationIds.Location = new System.Drawing.Point(320, 28);
this.lblStationIds.Name = "lblStationIds";
this.lblStationIds.Size = new System.Drawing.Size(79, 20);
this.lblStationIds.TabIndex = 3;
this.lblStationIds.Text = "站点集合:";
//
// txtStationIds
//
this.txtStationIds.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtStationIds.Location = new System.Drawing.Point(418, 24);
this.txtStationIds.Name = "txtStationIds";
this.txtStationIds.Size = new System.Drawing.Size(320, 25);
this.txtStationIds.TabIndex = 4;
//
// lblControlRight
//
this.lblControlRight.AutoSize = true;
this.lblControlRight.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblControlRight.Location = new System.Drawing.Point(12, 58);
this.lblControlRight.Name = "lblControlRight";
this.lblControlRight.Size = new System.Drawing.Size(65, 20);
this.lblControlRight.TabIndex = 5;
this.lblControlRight.Text = "控制权:";
//
// txtControlRight
//
this.txtControlRight.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtControlRight.Location = new System.Drawing.Point(100, 54);
this.txtControlRight.Name = "txtControlRight";
this.txtControlRight.Size = new System.Drawing.Size(200, 25);
this.txtControlRight.TabIndex = 6;
//
// lblIsOccupied
//
this.lblIsOccupied.AutoSize = true;
this.lblIsOccupied.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblIsOccupied.Location = new System.Drawing.Point(320, 58);
this.lblIsOccupied.Name = "lblIsOccupied";
this.lblIsOccupied.Size = new System.Drawing.Size(93, 20);
this.lblIsOccupied.TabIndex = 7;
this.lblIsOccupied.Text = "是否被占用:";
//
// chkIsOccupied
//
this.chkIsOccupied.AutoSize = true;
this.chkIsOccupied.Font = new System.Drawing.Font("Segoe UI", 9F);
this.chkIsOccupied.Location = new System.Drawing.Point(418, 59);
this.chkIsOccupied.Name = "chkIsOccupied";
this.chkIsOccupied.Size = new System.Drawing.Size(39, 19);
this.chkIsOccupied.TabIndex = 8;
this.chkIsOccupied.Text = "是";
//
// lblIsEnabled
//
this.lblIsEnabled.AutoSize = true;
this.lblIsEnabled.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblIsEnabled.Location = new System.Drawing.Point(500, 58);
this.lblIsEnabled.Name = "lblIsEnabled";
this.lblIsEnabled.Size = new System.Drawing.Size(79, 20);
this.lblIsEnabled.TabIndex = 9;
this.lblIsEnabled.Text = "是否启用:";
//
// chkIsEnabled
//
this.chkIsEnabled.AutoSize = true;
this.chkIsEnabled.Checked = true;
this.chkIsEnabled.CheckState = System.Windows.Forms.CheckState.Checked;
this.chkIsEnabled.Font = new System.Drawing.Font("Segoe UI", 9F);
this.chkIsEnabled.Location = new System.Drawing.Point(585, 59);
this.chkIsEnabled.Name = "chkIsEnabled";
this.chkIsEnabled.Size = new System.Drawing.Size(39, 19);
this.chkIsEnabled.TabIndex = 10;
this.chkIsEnabled.Text = "是";
//
// btnSave
//
this.btnSave.BackColor = System.Drawing.Color.LightBlue;
this.btnSave.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSave.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Bold);
this.btnSave.Location = new System.Drawing.Point(260, 118);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(140, 40);
this.btnSave.TabIndex = 12;
this.btnSave.Text = "保存";
this.btnSave.UseVisualStyleBackColor = false;
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// btnRefresh
//
this.btnRefresh.BackColor = System.Drawing.SystemColors.Control;
this.btnRefresh.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnRefresh.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnRefresh.Location = new System.Drawing.Point(410, 118);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(140, 40);
this.btnRefresh.TabIndex = 13;
this.btnRefresh.Text = "刷新";
this.btnRefresh.UseVisualStyleBackColor = false;
this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click);
//
// btnNew
//
this.btnNew.BackColor = System.Drawing.SystemColors.Control;
this.btnNew.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnNew.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnNew.Location = new System.Drawing.Point(560, 118);
this.btnNew.Name = "btnNew";
this.btnNew.Size = new System.Drawing.Size(140, 40);
this.btnNew.TabIndex = 14;
this.btnNew.Text = "新增";
this.btnNew.UseVisualStyleBackColor = false;
this.btnNew.Click += new System.EventHandler(this.btnNew_Click);
//
// btnDelete
//
this.btnDelete.BackColor = System.Drawing.SystemColors.Control;
this.btnDelete.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnDelete.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnDelete.Location = new System.Drawing.Point(110, 118);
this.btnDelete.Name = "btnDelete";
this.btnDelete.Size = new System.Drawing.Size(140, 40);
this.btnDelete.TabIndex = 11;
this.btnDelete.Text = "删除";
this.btnDelete.UseVisualStyleBackColor = false;
this.btnDelete.Click += new System.EventHandler(this.btnDelete_Click);
//
// TrafficInterlockViewer
//
this.ClientSize = new System.Drawing.Size(784, 521);
this.Controls.Add(this.lstTasks);
this.Controls.Add(this.grpEdit);
this.Font = new System.Drawing.Font("微软雅黑", 9F);
this.MinimumSize = new System.Drawing.Size(700, 450);
this.Name = "TrafficInterlockViewer";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "交通联锁区域管理";
this.grpEdit.ResumeLayout(false);
this.grpEdit.PerformLayout();
this.ResumeLayout(false);
}
}
}
@@ -1,235 +1,336 @@
using Newtonsoft.Json;
using StandardScene.InterLock; // 数据类型采用 TrafficInterlockMission 中的 TrafficArea
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 StandardScene.InterLock;
using StandardScene.Utils;
namespace LoopViewerApp
{
public partial class TrafficInterlockViewer : Form
/// <summary>
/// 交通联锁区域管理界面(CycleGUI 版,替代原 WinForms <c>TrafficInterlockViewer</c> 窗体)。
/// <list type="bullet">
/// <item>维护 <see cref="TrafficInterlockMission.TrafficAreaList"/> 的增 / 改 / 删,并写入 <c>Config/traffic.json</c>。</item>
/// <item>单实例:再次打开则把已有面板置前。</item>
/// <item>勾选多行后「删除选中」可批量删除;每行「编辑」按钮加载下方编辑区。</item>
/// <item>文件写入放后台线程,绝不阻塞渲染线程。</item>
/// </list>
/// 沿用 <c>LoopViewer</c> / <c>DeliveryViewer</c> 同套模式(单实例面板、<c>pb.Table</c>、<c>CycleUiHelper.ConfirmThen</c>)。
/// 保留可实例化 + <see cref="Show"/> 以兼容既有调用 <c>new TrafficInterlockViewer().Show()</c>。
/// </summary>
public class TrafficInterlockViewer
{
/// <summary>-1 表示新增模式;>=0 表示正在编辑对应索引</summary>
private int _editingIndex = -1;
private const string TableId = "traffic-area-list";
/// <summary>选中行变化时是否允许加载到编辑区(避免在保存/取消时重复刷新)</summary>
private bool _allowLoadFromSelection = true;
private static string JsonPath =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config", "traffic.json");
public TrafficInterlockViewer()
private static readonly object SaveLock = new object();
private static Panel _panel;
private static string _editingAreaId = "";
private static readonly HashSet<string> _selected = new HashSet<string>(StringComparer.Ordinal);
private static string _areaName = "";
private static string _stationIds = "";
private static string _controlRight = "";
private static bool _isOccupied;
private static bool _isEnabled = true;
private static string _editingHint = "新增区域";
private static string _editErr = "";
private static volatile string _status = "";
/// <summary>打开(或置前)区域管理面板。兼容原 <c>new TrafficInterlockViewer().Show()</c> 调用方式。</summary>
public void Show() => Open();
/// <summary>打开(或置前)区域管理面板。</summary>
public static void Open()
{
InitializeComponent();
if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
return;
try
if (_panel != null)
{
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"TrafficInterlockViewer init error: {ex}");
}
}
#region
private void lstTasks_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
try
{
// 与 LoopViewer 一致:深蓝表头 + 白色加粗字体
using (var backBrush = new SolidBrush(Color.FromArgb(63, 81, 181)))
using (var textBrush = new SolidBrush(Color.White))
using (var font = new Font("微软雅黑", 9, FontStyle.Bold))
try
{
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);
_panel.BringToFront();
return;
}
catch
{
_panel = null;
}
}
catch
ClearPanelInputs();
var panel = GUI.DeclarePanel()
.ShowTitle("交通联锁区域管理")
.SetDefaultDocking(Panel.Docking.None)
.InitSize(800, 680)
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_panel = panel;
panel.IfTerminalQuit(() => { if (_panel == panel) _panel = null; });
panel.Define(pb =>
{
e.DrawBackground();
e.DrawText();
}
if (pb.Closing())
{
panel.Exit();
_panel = null;
return;
}
List<TrafficArea> areas;
lock (TrafficInterlockMission.TrafficAreaList)
{
EnsureAreaIdsLocked();
areas = TrafficInterlockMission.TrafficAreaList.ToList();
}
_selected.RemoveWhere(id => areas.All(a => a.Id != id));
if (!string.IsNullOrEmpty(_editingAreaId) && areas.All(a => a.Id != _editingAreaId))
ClearPanelInputs();
if (pb.Button("删除选中", distinct: "traffic-del-selected"))
ConfirmDeleteSelected();
pb.SameLine(12);
if (pb.Button("刷新", distinct: "traffic-refresh"))
{
_status = "";
_editErr = "";
}
pb.SameLine(12);
pb.Label($"共 {areas.Count} 个区域,已选 {_selected.Count} 个");
pb.Table(TableId,
new[] { "选择", "区域名称", "区域站点集合", "控制权", "是否被占用", "是否启用", "操作" },
areas.Count, (row, i) =>
{
var a = areas[i];
var areaId = EnsureAreaId(a);
var sel = _selected.Contains(areaId);
if (row.Checkbox(ref sel, "勾选以批量删除"))
{
if (sel) _selected.Add(areaId);
else _selected.Remove(areaId);
}
row.Label(a.AreaName ?? "");
row.Label(a.SiteList != null && a.SiteList.Count > 0
? string.Join(", ", a.SiteList)
: "");
row.Label(a.ControllerName ?? "");
row.Label(a.IsOccupy ? "是" : "否");
row.Label(a.IsEnable ? "是" : "否");
if (row.ButtonGroup(new[] { "编辑" }, new[] { "编辑该区域" }) == 0)
{
_editingAreaId = areaId;
LoadAreaToFields(a);
}
}, height: 14, enableSearch: true);
pb.Separator();
pb.Label("数据新增/编辑(点击表格行「编辑」可在此查看并修改该行数据)");
pb.Label(_editingHint);
var (name, _) = pb.TextInput("1. 区域名称", _areaName, alwaysReturnString: true);
_areaName = name;
var (sites, _) = pb.TextInput("2. 站点集合 (逗号/分号/空格分隔)", _stationIds, alwaysReturnString: true);
_stationIds = sites;
var (ctrl, _) = pb.TextInput("3. 控制权", _controlRight, alwaysReturnString: true);
_controlRight = ctrl;
pb.CheckBox("4. 是否被占用", ref _isOccupied);
pb.SameLine(16);
pb.CheckBox("5. 是否启用", ref _isEnabled);
if (!string.IsNullOrEmpty(_editErr))
{
pb.Separator();
pb.Label(_editErr);
}
pb.Separator();
if (pb.Button("保存", distinct: "traffic-save"))
SaveArea();
pb.SameLine(12);
if (pb.Button("新增", distinct: "traffic-new"))
{
_selected.Clear();
ClearPanelInputs();
}
if (!string.IsNullOrEmpty(_status))
{
pb.Separator();
pb.Label(_status);
}
pb.Panel.Repaint(repaintTimeMs: 500);
});
}
private void lstTasks_DrawItem(object sender, DrawListViewItemEventArgs e)
private static void ConfirmDeleteSelected()
{
// 由 DrawSubItem 统一绘制
if (_selected.Count == 0)
{
_status = "请先在上方列表中选择要删除的区域。";
_panel?.Repaint();
return;
}
string prompt = _selected.Count == 1
? "确定要删除选中的区域吗?"
: $"确定要删除所选 {_selected.Count} 个区域吗?";
CycleUiHelper.ConfirmThen(prompt, DeleteSelected);
}
private void lstTasks_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
private static void DeleteSelected()
{
var selectedIds = _selected.ToHashSet(StringComparer.Ordinal);
var removed = 0;
lock (TrafficInterlockMission.TrafficAreaList)
{
removed = TrafficInterlockMission.TrafficAreaList.RemoveAll(a => selectedIds.Contains(a.Id));
}
_selected.Clear();
SaveToConfig();
ClearPanelInputs();
_status = $"已删除 {removed} 个区域";
_panel?.Repaint();
}
private static void SaveArea()
{
try
{
bool selected = e.Item.Selected;
Rectangle bounds = e.Bounds;
// 与 LoopViewer 一致:选中行蓝色强调,交替行背景,深灰文字
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)
string areaName = (_areaName ?? "").Trim();
if (string.IsNullOrEmpty(areaName))
{
using (var selBrush = new SolidBrush(selectedBack))
e.Graphics.FillRectangle(selBrush, bounds);
_editErr = "请输入区域名称。";
_panel?.Repaint();
return;
}
var stationIds = ParseStationIds(_stationIds ?? "");
string controlRight = (_controlRight ?? "").Trim();
bool isOccupied = _isOccupied;
bool isEnabled = _isEnabled;
if (!string.IsNullOrEmpty(_editingAreaId))
{
lock (TrafficInterlockMission.TrafficAreaList)
{
var existing = TrafficInterlockMission.TrafficAreaList
.FirstOrDefault(a => string.Equals(a.Id, _editingAreaId, StringComparison.Ordinal));
if (existing == null)
{
_editErr = "正在编辑的区域已不存在,请刷新后重试。";
_panel?.Repaint();
return;
}
existing.AreaName = areaName;
existing.SiteList = stationIds;
existing.ControllerName = controlRight;
existing.IsOccupy = isOccupied;
existing.IsEnable = isEnabled;
}
_status = $"已保存区域:{areaName}";
}
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;
var textRect = bounds;
textRect.Inflate(-6, 0);
using (var font = new Font("微软雅黑", 9))
TextRenderer.DrawText(e.Graphics, text, font, textRect, fore, TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
#endregion
#region
private void RenderListView()
{
try
{
if (lstTasks == null) return;
_allowLoadFromSelection = false;
lstTasks.BeginUpdate();
lstTasks.Items.Clear();
foreach (var a in TrafficInterlockMission.TrafficAreaList)
{
var stationStr = a.SiteList != null && a.SiteList.Count > 0 ? string.Join(", ", a.SiteList) : "";
var lvi = new ListViewItem(new[]
lock (TrafficInterlockMission.TrafficAreaList)
{
a.AreaName ?? "",
stationStr,
a.ControllerName ?? "",
a.IsOccupy ? "是" : "否",
a.IsEnable ? "是" : "否"
});
lstTasks.Items.Add(lvi);
TrafficInterlockMission.TrafficAreaList.Add(new TrafficArea
{
Id = NewAreaId(),
AreaName = areaName,
SiteList = stationIds,
ControllerName = controlRight,
IsOccupy = isOccupied,
IsEnable = isEnabled
});
}
_status = "已新增区域";
}
lstTasks.EndUpdate();
_allowLoadFromSelection = true;
SaveToConfig();
ClearPanelInputs();
_editErr = "";
_panel?.Repaint();
}
catch (Exception ex)
{
_allowLoadFromSelection = true;
System.Diagnostics.Debug.WriteLine($"RenderListView failed: {ex}");
_editErr = "操作失败:" + ex.Message;
_panel?.Repaint();
}
}
private void SaveToConfig()
{
try
{
string configPath = Path.Combine(Application.StartupPath, "Config", "traffic.json");
string dir = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllText(configPath, JsonConvert.SerializeObject(TrafficInterlockMission.TrafficAreaList, Formatting.Indented));
}
catch (Exception ex)
{
MessageBox.Show("保存失败:" + ex.Message);
}
}
#endregion
#region
private void lstTasks_SelectedIndexChanged(object sender, EventArgs e)
{
if (!_allowLoadFromSelection || lstTasks == null || lstTasks.SelectedIndices.Count == 0) return;
int idx = lstTasks.SelectedIndices[0];
if (idx < 0 || idx >= TrafficInterlockMission.TrafficAreaList.Count) return;
_editingIndex = idx;
LoadAreaToPanel(TrafficInterlockMission.TrafficAreaList[idx]);
}
#endregion
#region /
private void LoadAreaToPanel(TrafficArea a)
private static void LoadAreaToFields(TrafficArea a)
{
if (a == null) return;
try
{
if (lblEditingHint != null)
lblEditingHint.Text = $"编辑:{a.AreaName}";
if (txtAreaName != null)
txtAreaName.Text = a.AreaName ?? "";
if (txtStationIds != null)
txtStationIds.Text = a.SiteList != null && a.SiteList.Count > 0
? string.Join(", ", a.SiteList)
: "";
if (txtControlRight != null)
txtControlRight.Text = a.ControllerName ?? "";
if (chkIsOccupied != null)
chkIsOccupied.Checked = a.IsOccupy;
if (chkIsEnabled != null)
chkIsEnabled.Checked = a.IsEnable;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoadAreaToPanel error: {ex}");
}
_editingHint = $"编辑:{a.AreaName}";
_areaName = a.AreaName ?? "";
_stationIds = a.SiteList != null && a.SiteList.Count > 0
? string.Join(", ", a.SiteList)
: "";
_controlRight = a.ControllerName ?? "";
_isOccupied = a.IsOccupy;
_isEnabled = a.IsEnable;
_editErr = "";
}
private void ClearPanelInputs()
private static void ClearPanelInputs()
{
_editingAreaId = "";
_editingHint = "新增区域";
_areaName = "";
_stationIds = "";
_controlRight = "";
_isOccupied = false;
_isEnabled = true;
_editErr = "";
}
private static void SaveToConfig()
{
string json;
try
{
_editingIndex = -1;
if (lblEditingHint != null)
lblEditingHint.Text = "新增区域";
if (txtAreaName != null)
txtAreaName.Text = "";
if (txtStationIds != null)
txtStationIds.Text = "";
if (txtControlRight != null)
txtControlRight.Text = "";
if (chkIsOccupied != null)
chkIsOccupied.Checked = false;
if (chkIsEnabled != null)
chkIsEnabled.Checked = true;
lock (TrafficInterlockMission.TrafficAreaList)
json = JsonConvert.SerializeObject(TrafficInterlockMission.TrafficAreaList, Formatting.Indented);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ClearPanelInputs error: {ex}");
_status = "保存失败:" + ex.Message;
_panel?.Repaint();
return;
}
var path = JsonPath;
var dir = Path.GetDirectoryName(path);
Task.Run(() =>
{
try
{
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
lock (SaveLock)
File.WriteAllText(path, json);
}
catch (Exception ex)
{
_status = "保存失败:" + ex.Message;
_panel?.Repaint();
}
});
}
#endregion
#region "1,2,3" -> List<int>
/// <summary>解析站点集合字符串,如 "1,2,3" → <see cref="List{T}"/> of int。</summary>
private static List<int> ParseStationIds(string text)
{
var list = new List<int>();
@@ -242,129 +343,19 @@ namespace LoopViewerApp
return list;
}
#endregion
#region / / /
private void btnSave_Click(object sender, EventArgs e)
private static void EnsureAreaIdsLocked()
{
try
{
string areaName = txtAreaName?.Text?.Trim() ?? "";
if (string.IsNullOrEmpty(areaName))
{
MessageBox.Show("请输入区域名称。");
return;
}
var stationIds = ParseStationIds(txtStationIds?.Text ?? "");
string controlRight = txtControlRight?.Text?.Trim() ?? "";
bool isOccupied = chkIsOccupied?.Checked ?? false;
bool isEnabled = chkIsEnabled?.Checked ?? true;
if (_editingIndex >= 0 && _editingIndex < TrafficInterlockMission.TrafficAreaList.Count)
{
lock (TrafficInterlockMission.TrafficAreaList)
{
var existing = TrafficInterlockMission.TrafficAreaList[_editingIndex];
existing.AreaName = areaName;
existing.SiteList = stationIds;
existing.ControllerName = controlRight;
existing.IsOccupy = isOccupied;
existing.IsEnable = isEnabled;
}
}
else
{
lock (TrafficInterlockMission.TrafficAreaList)
{
TrafficInterlockMission.TrafficAreaList.Add(new TrafficArea
{
AreaName = areaName,
SiteList = stationIds,
ControllerName = controlRight,
IsOccupy = isOccupied,
IsEnable = isEnabled
});
}
}
SaveToConfig();
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnSave_Click error: {ex}");
MessageBox.Show("操作失败:" + ex.Message);
}
foreach (var area in TrafficInterlockMission.TrafficAreaList)
EnsureAreaId(area);
}
private void btnRefresh_Click(object sender, EventArgs e)
private static string EnsureAreaId(TrafficArea area)
{
try
{
RenderListView();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnRefresh_Click error: {ex}");
}
if (string.IsNullOrWhiteSpace(area.Id))
area.Id = NewAreaId();
return area.Id;
}
private void btnNew_Click(object sender, EventArgs e)
{
if (lstTasks != null)
lstTasks.SelectedIndices.Clear();
ClearPanelInputs();
// 进入新增模式:填写下方编辑区后点击“保存”即可新增一条数据
}
private void btnDelete_Click(object sender, EventArgs e)
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0)
{
MessageBox.Show("请先在上方列表中选择要删除的区域。");
return;
}
var dialogResult = MessageBox.Show(
"确定要删除选中的区域吗?",
"确认删除",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (dialogResult != DialogResult.Yes)
return;
var indices = lstTasks.SelectedIndices.Cast<int>()
.OrderByDescending(i => i)
.ToList();
lock (TrafficInterlockMission.TrafficAreaList)
{
foreach (var idx in indices)
{
if (idx >= 0 && idx < TrafficInterlockMission.TrafficAreaList.Count)
{
TrafficInterlockMission.TrafficAreaList.RemoveAt(idx);
}
}
}
SaveToConfig();
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnDelete_Click error: {ex}");
MessageBox.Show("删除失败:" + ex.Message);
}
}
#endregion
private static string NewAreaId() => Guid.NewGuid().ToString("N");
}
}
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>