using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using CycleGUI; using Newtonsoft.Json; using StandardScene.Utils; namespace StandardScene.Signal.Ui { /// /// 按 Model 的 反射生成可增删改表格。 /// /// /// 列顺序取属性 MetadataToken(源码声明顺序)。 /// 跳过不可写、索引器、 为 false、 的属性。 /// 编辑区用独立字典暂存,点「添加」或「保存当前行」才反写对象并 。 /// CycleGUI 控件 id 带 _idPrefix,三种表同时开窗时不会抢同一组控件状态。 /// public sealed class ModelTableEditor where T : class, new() { private static PropertyInfo[] Props => _props ??= Discover(); private static PropertyInfo[] _props; private List _items = new List(); /// -1 表示未选中,此时「保存/删除」会提示先选行,「添加」写入新行。 private int _selected = -1; private string _status = ""; private string _path = ""; private string _idPrefix = typeof(T).Name; private Func _keySelector = _ => ""; /// 字符串/数值编辑缓存,键为属性名。 private readonly Dictionary _text = new Dictionary(); private readonly Dictionary _bools = new Dictionary(); /// 枚举下拉当前下标,写入时按 Enum.GetNames 转回枚举值。 private readonly Dictionary _enumIdx = new Dictionary(); public int Count => _items.Count; public string Status => _status; /// 绑定 JSON 路径并立即 Reload。同一编辑器换文件时调用。 public void Bind(string path, Func keySelector, string idPrefix) { _path = path ?? ""; _keySelector = keySelector ?? (_ => ""); _idPrefix = string.IsNullOrWhiteSpace(idPrefix) ? typeof(T).Name : idPrefix; Reload(); } /// 从磁盘重读,丢弃未保存的编辑区内容。 public void Reload() { _items = SignalConfigStore.Load(_path); _selected = -1; _status = $"已加载 {_items.Count} 条 · {_path}"; ClearEditor(); } /// 列表 + 编辑区 + 添加/保存/删除/清空/刷新。每帧由面板 Define 调用。 public void Render(PanelBuilder pb) { var headers = Props.Select(DisplayOf).Concat(new[] { "行操作" }).ToArray(); pb.SeparatorText($"列表({_items.Count})"); pb.Table($"dc-list-{_idPrefix}", headers, _items.Count, (row, i) => { var item = _items[i]; foreach (var p in Props) row.Label(FormatCell(p.GetValue(item))); if (row.ButtonGroup(new[] { "选择" }, new[] { "选择该行" }) == 0) { _selected = i; LoadEditor(item); } }, height: 10, enableSearch: true); pb.SeparatorText(_selected >= 0 ? $"编辑第 {_selected + 1} 行" : "编辑(未选中则「添加」写入新行)"); DrawEditor(pb); if (pb.Button("添加", distinct: $"dc-{_idPrefix}-add")) Add(); pb.SameLine(8); if (pb.Button("保存当前行", distinct: $"dc-{_idPrefix}-save")) SaveRow(); pb.SameLine(8); if (pb.Button("删除当前行", distinct: $"dc-{_idPrefix}-del")) ConfirmDelete(); pb.SameLine(8); if (pb.Button("清空编辑区", distinct: $"dc-{_idPrefix}-clear")) { _selected = -1; ClearEditor(); _status = "已清空编辑区,可添加新行"; } pb.SameLine(8); if (pb.Button("从文件刷新", distinct: $"dc-{_idPrefix}-reload")) Reload(); if (!string.IsNullOrEmpty(_status)) { pb.Separator(); pb.Label(_status); } } /// 发现可编辑列。Browsable=false 或 JsonIgnore 的运行态字段不会进表。 private static PropertyInfo[] Discover() { return typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0) .Where(p => p.GetCustomAttribute()?.Browsable != false) .Where(p => p.GetCustomAttribute() == null) .OrderBy(p => p.MetadataToken) .ToArray(); } private static string DisplayOf(PropertyInfo p) => p.GetCustomAttribute()?.DisplayName ?? p.Name; private static string FormatCell(object value) { if (value == null) return ""; if (value is bool b) return b ? "是" : "否"; return Convert.ToString(value) ?? ""; } private void DrawEditor(PanelBuilder pb) { for (var i = 0; i < Props.Length; i++) { var p = Props[i]; var label = $"{_idPrefix}-{i + 1}. {DisplayOf(p)}"; var type = p.PropertyType; if (type == typeof(bool)) { if (!_bools.ContainsKey(p.Name)) _bools[p.Name] = false; var v = _bools[p.Name]; pb.CheckBox(label, ref v); _bools[p.Name] = v; } else if (type.IsEnum) { var names = Enum.GetNames(type); if (names.Length == 0) continue; if (!_enumIdx.ContainsKey(p.Name)) _enumIdx[p.Name] = 0; var idx = _enumIdx[p.Name]; if (idx < 0 || idx >= names.Length) idx = 0; pb.DropdownBox(label, names, ref idx); _enumIdx[p.Name] = idx; } else { if (!_text.ContainsKey(p.Name)) _text[p.Name] = ""; var (text, _) = pb.TextInput(label, _text[p.Name], alwaysReturnString: true); _text[p.Name] = text ?? ""; } } } private void LoadEditor(T item) { foreach (var p in Props) { var value = p.GetValue(item); if (p.PropertyType == typeof(bool)) _bools[p.Name] = value is true; else if (p.PropertyType.IsEnum) { var names = Enum.GetNames(p.PropertyType); var name = value?.ToString() ?? names.FirstOrDefault() ?? ""; var idx = Array.IndexOf(names, name); _enumIdx[p.Name] = idx >= 0 ? idx : 0; } else _text[p.Name] = value == null ? "" : Convert.ToString(value) ?? ""; } } private void ClearEditor() { _text.Clear(); _bools.Clear(); _enumIdx.Clear(); foreach (var p in Props) { if (p.PropertyType == typeof(bool)) _bools[p.Name] = false; else if (p.PropertyType.IsEnum) _enumIdx[p.Name] = 0; else if (p.PropertyType == typeof(int) || p.PropertyType == typeof(byte) || p.PropertyType == typeof(ushort)) _text[p.Name] = "0"; else _text[p.Name] = DefaultText(p); } } private static string DefaultText(PropertyInfo p) { try { var fresh = new T(); var v = p.GetValue(fresh); return v == null ? "" : Convert.ToString(v) ?? ""; } catch { return ""; } } /// 把编辑区打成对象。主键空、整数解析失败时返回 false 并给出中文 err。 private bool TryBuild(out T item, out string err) { item = new T(); err = ""; foreach (var p in Props) { try { object boxed; if (p.PropertyType == typeof(bool)) boxed = _bools.TryGetValue(p.Name, out var b) && b; else if (p.PropertyType.IsEnum) { var names = Enum.GetNames(p.PropertyType); var idx = _enumIdx.TryGetValue(p.Name, out var e) ? e : 0; if (idx < 0 || idx >= names.Length) idx = 0; boxed = Enum.Parse(p.PropertyType, names[idx]); } else if (p.PropertyType == typeof(int)) { var raw = _text.TryGetValue(p.Name, out var t) ? t : "0"; if (!int.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n)) { err = $"{DisplayOf(p)} 必须是整数"; return false; } boxed = n; } else if (p.PropertyType == typeof(ushort)) { var raw = _text.TryGetValue(p.Name, out var t) ? t : "0"; if (!ushort.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n)) { err = $"{DisplayOf(p)} 必须是 0-65535"; return false; } boxed = n; } else if (p.PropertyType == typeof(byte)) { var raw = _text.TryGetValue(p.Name, out var t) ? t : "0"; if (!byte.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n)) { err = $"{DisplayOf(p)} 必须是 0-255"; return false; } boxed = n; } else boxed = _text.TryGetValue(p.Name, out var s) ? s ?? "" : ""; p.SetValue(item, boxed); } catch (Exception ex) { err = $"{DisplayOf(p)} 写入失败:{ex.Message}"; return false; } } var key = (_keySelector(item) ?? "").Trim(); if (string.IsNullOrEmpty(key)) { err = "主键不能为空"; return false; } return true; } /// 主键已存在则拒绝。成功后立刻写盘并把选中行指到新行。 private void Add() { if (!TryBuild(out var item, out var err)) { CycleUiHelper.Alert("错误", err); return; } var key = _keySelector(item); if (_items.Any(x => string.Equals(_keySelector(x), key, StringComparison.OrdinalIgnoreCase))) { CycleUiHelper.Alert("错误", $"主键「{key}」已存在"); return; } _items.Add(item); _selected = _items.Count - 1; SignalConfigStore.Save(_path, _items); _status = $"已添加 {key}"; } /// 覆盖当前选中行。主键与其它行冲突则拒绝。 private void SaveRow() { if (_selected < 0 || _selected >= _items.Count) { CycleUiHelper.Alert("提示", "请先选择要保存的行,或用「添加」写入新行"); return; } if (!TryBuild(out var item, out var err)) { CycleUiHelper.Alert("错误", err); return; } var key = _keySelector(item); if (_items.Where((_, i) => i != _selected) .Any(x => string.Equals(_keySelector(x), key, StringComparison.OrdinalIgnoreCase))) { CycleUiHelper.Alert("错误", $"主键「{key}」已存在"); return; } _items[_selected] = item; SignalConfigStore.Save(_path, _items); _status = $"已保存 {key}"; CycleUiHelper.Alert("提示", "保存成功"); } /// 确认框通过后删除选中行并写盘。 private void ConfirmDelete() { if (_selected < 0 || _selected >= _items.Count) { CycleUiHelper.Alert("提示", "请先选择要删除的行"); return; } var key = _keySelector(_items[_selected]); CycleUiHelper.ConfirmThen($"删除「{key}」?", () => { _items.RemoveAt(_selected); _selected = -1; ClearEditor(); SignalConfigStore.Save(_path, _items); _status = $"已删除 {key}"; }); } } }